From 2d497e5195df4fcfa961913e55b13cfd6c015852 Mon Sep 17 00:00:00 2001 From: Adrian Cowan Date: Sat, 7 Jun 2025 09:07:48 +1000 Subject: [PATCH] Add Nomad job configuration for Teams Status Updater --- 2-nomad-config/teamsstatus.nomad.hcl | 345 +++++++++++++++++++++++++++ 2-nomad-config/teamsstatus.tf | 10 + 2 files changed, 355 insertions(+) create mode 100644 2-nomad-config/teamsstatus.nomad.hcl create mode 100644 2-nomad-config/teamsstatus.tf diff --git a/2-nomad-config/teamsstatus.nomad.hcl b/2-nomad-config/teamsstatus.nomad.hcl new file mode 100644 index 0000000..f8833d7 --- /dev/null +++ b/2-nomad-config/teamsstatus.nomad.hcl @@ -0,0 +1,345 @@ +job "teamsstatus" { + group "app" { + task "teamsstatus" { + driver = "docker" + + config { + image = "python:3.11-slim" + command = "/local/start.sh" + } + + # Template for the startup script + template { + data = <&1 +EOF + destination = "local/start.sh" + perms = "755" + } + + # Template for the token cache + template { + data = "{{ with nomadVar \"nomad/jobs/teamsstatus\" }}{{ .token_cache_json }}{{ end }}" + destination = "local/token_cache.json" + } + + # Template for the Python script + template { + data = <", + "contentType": "text", + } + }, + "expirationDateTime": { + "dateTime": expiration_date_time, + "timeZone": time_zone + }, + } + + headers = { + 'Authorization': f'Bearer {access_token}', + 'Content-Type': 'application/json' + } + + logging.debug(f"Setting status message for user {user_id}") + + response = requests.post(url, json=payload, headers=headers) + + if response.status_code == 200: + logging.info(f"Teams status message set to: {status_message}") + return True + else: + logging.error(f"Failed to set Teams status message: {response.status_code}") + return False + +def _load_segments(): + """Load the journey segments from embedded data into memory""" + global _segments + if _segments: # Already loaded + return + + aest = timezone(timedelta(hours=10)) + + for line in JOURNEY_DATA.split('\n')[1:]: # Skip header + day, start_time, end_time, start_dist, end_dist, start_loc, end_loc = line.strip().split('\t') + + # Convert day and times to datetime in AEST + day_start = datetime.strptime(f"{day} {start_time}", "%d/%m/%Y %H:%M:%S").replace(tzinfo=aest) + day_end = datetime.strptime(f"{day} {end_time}", "%d/%m/%Y %H:%M:%S").replace(tzinfo=aest) + + # Extract the numeric distance values + start_dist = int(start_dist.rstrip('km')) + end_dist = int(end_dist.rstrip('km')) + + _segments.append({ + 'start_time': day_start, + 'end_time': day_end, + 'start_dist': start_dist, + 'end_dist': end_dist, + 'start_location': start_loc, + 'end_location': end_loc + }) + +def get_trip_info(target_datetime): + """Determine the distance travelled and locations for the current datetime.""" + if target_datetime.tzinfo is None: + raise ValueError("target_datetime must be timezone-aware") + + # Ensure data is loaded + _load_segments() + + # Before journey starts + if not _segments or target_datetime < _segments[0]['start_time']: + start_loc = end_loc = _segments[0]['start_location'] + return (0, start_loc, end_loc) + + # During journey + for i, segment in enumerate(_segments): + # If target is before this segment starts + if target_datetime < segment['start_time']: + prev_segment = _segments[i-1] + return (prev_segment['end_dist'], prev_segment['end_location'], prev_segment['end_location']) + + # If target is during this segment, interpolate + if segment['start_time'] <= target_datetime <= segment['end_time']: + # Calculate what fraction of the segment has elapsed + total_seconds = (segment['end_time'] - segment['start_time']).total_seconds() + elapsed_seconds = (target_datetime - segment['start_time']).total_seconds() + fraction = elapsed_seconds / total_seconds + + # Interpolate the distance + distance_delta = segment['end_dist'] - segment['start_dist'] + current_dist = segment['start_dist'] + int(distance_delta * fraction) + return (current_dist, segment['start_location'], segment['end_location']) + + # Between segments + if i < len(_segments) - 1: + next_segment = _segments[i + 1] + if segment['end_time'] < target_datetime < next_segment['start_time']: + return (segment['end_dist'], segment['end_location'], segment['end_location']) + + # After journey ends + return (_segments[-1]['end_dist'], _segments[-1]['end_location'], _segments[-1]['end_location']) + +def build_message(distance, start_loc, end_loc): + """Build the status message based on distance and locations""" + message = "On leave" + if distance > 13144: + message += f", driving my EV back from WA" + if distance > 2118: + message += f", driving my EV around WA" + elif distance > 0: + message += f", driving my EV to WA" + + if distance > 0: + distance += random.randint(-5, 5) + message += f", {distance}kms travelled so far" + if start_loc != end_loc: + message += f", next stop {end_loc}" + else: + message += f", near {start_loc}" + + message += ", returning July 21st. Contacts {CIM: Grant Gorfine, Inserts: Daniel Pate, DevOps: Rob Duncan, else: Andrian Zubovic}" + return message + +def main(): + test_mode = False # Set to True to run in test mode + time_scale = 1 # 1/600 # Set to 1/60 to run at 1 second per minute, 1 for normal speed + + # Set start time to 7:30 AM AEST (UTC+10) on June 8th, 2025 + aest = timezone(timedelta(hours=10)) + start_time = datetime.now(aest) + date_offset = datetime(2025, 6, 8, 7, 30, 0, tzinfo=aest) - start_time + + if test_mode: + logging.info("Running in test mode - status messages will not actually be set") + + app = get_msal_app(client_id = "e6cda941-949f-495e-88f5-10eb45ffa0e7") + + last_token_refresh = 0 + # Token refresh interval (60 minutes in seconds) + TOKEN_REFRESH_INTERVAL = int(60 * 60) # Scale the 1 hour refresh interval + + old_distance = -1 + while True: + try: + # Check if we need to refresh the token + current_time = time.time() + if current_time - last_token_refresh >= TOKEN_REFRESH_INTERVAL or last_token_refresh == 0: + logging.info("Acquiring/refreshing access token...") + access_token = acquire_token(app, scope = ["https://graph.microsoft.com/Presence.ReadWrite"]) + if not access_token: + logging.error("Failed to acquire token") + exit(1) + last_token_refresh = current_time + logging.info("Token successfully refreshed") + + # Set the status message + now = datetime.now(aest) # Get current time in AEST + if time_scale != 1: + # Adjust the current time based on the time scale + now = start_time + (now - start_time) / time_scale + now += date_offset # Adjust to the target start time + distance, start_loc, end_loc = get_trip_info(now) # We only need distance for comparison + if distance != old_distance: + message = build_message(distance, start_loc, end_loc) + timestamp = now.strftime("%Y-%m-%d %H:%M:%S %Z") + if not test_mode: + logging.info(f"[{timestamp}] Message: {message}") + success = set_teams_status_message( + access_token = access_token, + user_id = "1b625872-d8a8-42f4-b237-dfa6d8062360", + status_message = message, + ) + else: + logging.info(f"[TEST MODE] [{timestamp}] Message: {message}") + success = True + else: + logging.debug("Status message has not changed, skipping update") + success = True + old_distance = distance + + if success: + wait_time = 900 * time_scale # Scale the 15 minute wait time + logging.debug(f"Waiting {wait_time} seconds before updating status message again...") + time.sleep(wait_time) + else: + last_token_refresh = 0 # Reset token refresh time on failure + except KeyboardInterrupt: + logging.info("Status update interrupted by user. Exiting...") + break + except Exception as e: + logging.error(f"An error occurred: {e}") + time.sleep(300) # Wait 5 minutes before retrying + + return 0 + +if __name__ == "__main__": + exit(main()) + +EOF + destination = "local/teamsstatus_standalone.py" + } + + resources { + cpu = 500 + memory = 256 + } + } + + restart { + attempts = 3 + interval = "5m" + delay = "15s" + mode = "fail" + } + } +} diff --git a/2-nomad-config/teamsstatus.tf b/2-nomad-config/teamsstatus.tf new file mode 100644 index 0000000..b8a5a49 --- /dev/null +++ b/2-nomad-config/teamsstatus.tf @@ -0,0 +1,10 @@ +resource "nomad_job" "teamsstatus" { + jobspec = file("${path.module}/teamsstatus.nomad.hcl") +} + +# resource "nomad_variable" "teamsstatus" { +# path = "nomad/jobs/teamsstatus" +# items = { +# token_cache_json = file("${path.module}/token_cache.json") +# } +# }