Skip to content

Commit

Permalink
[a 2/2] Announce maintenance in stable deployments (#5979)
Browse files Browse the repository at this point in the history
  • Loading branch information
achave11-ucsc committed Aug 22, 2024
1 parent c0a4727 commit 9916081
Show file tree
Hide file tree
Showing 5 changed files with 336 additions and 9 deletions.
41 changes: 41 additions & 0 deletions lambdas/service/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@
from azul.logging import (
configure_app_logging,
)
from azul.maintenance import (
MaintenanceService,
)
from azul.openapi import (
application_json,
format_description as fd,
Expand Down Expand Up @@ -870,6 +873,44 @@ def get_integrations():
body=json.dumps(body))


@app.route(
'/maintenance/schedule',
methods=['GET'],
cors=True,
method_spec={
'summary': 'A maintenance schedule as an JSON object',
'tags': ['Auxiliary'],
'responses': {
'200': {
'description': fd('''
This object may be hanceforth refered to as "the schedule"
or `schedule`.
The `start` time of an event is its `actual_start` if set,
or its `planned_start` otherwise. The `end` time of an event
is its `actual_end` if set, or its `start` plus
`planned_duration` otherwise. All events in the schedule are
sorted by their `start` time. No two events have the same
`start` time. Each event defines an interval
`[e.start, e.end)` and there is no overlap between these
intervals.
A pending event is one where `actual_start` is absent. An
active event is one where `actual_start` is present but
`actual_end` is absent. There can be at most one active
event.
''')
}
}
}
)
def get_maintenance_schedule():
service = MaintenanceService()
schedule = service.get_schedule.to_json()
return Response(status_code=200,
headers={'content-type': 'application/json'},
body=json.dumps(schedule))


@app.route(
'/index/catalogs',
methods=['GET'],
Expand Down
13 changes: 13 additions & 0 deletions lambdas/service/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,19 @@
}
}
},
"/maintenance/schedule": {
"get": {
"summary": "A maintenance schedule as an JSON object",
"tags": [
"Auxiliary"
],
"responses": {
"200": {
"description": "\nThis object may be hanceforth refered to as \"the schedule\"\nor `schedule`.\nThe `start` time of an event is its `actual_start` if set,\nor its `planned_start` otherwise. The `end` time of an event\nis its `actual_end` if set, or its `start` plus\n`planned_duration` otherwise. All events in the schedule are\nsorted by their `start` time. No two events have the same\n`start` time. Each event defines an interval\n`[e.start, e.end)` and there is no overlap between these\n intervals.\n\n A pending event is one where `actual_start` is absent. An\n active event is one where `actual_start` is present but\n `actual_end` is absent. There can be at most one active\n event.\n"
}
}
}
},
"/index/catalogs": {
"get": {
"summary": "List all available catalogs.",
Expand Down
142 changes: 142 additions & 0 deletions scripts/manage_maintenance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""
This is a command line utility for managing announcement of maintenance events.
Reads the JSON from designated bucket, deserializes the model from it, validates
the model, applies an action to it, serializes the model back to JSON and
finally uploads it back to the bucket where the service exposes it. The service
must also validate the model before returning it.
"""
import argparse
from datetime import (
timedelta,
)
import json
import re
import sys

from azul import (
require,
)
from azul.args import (
AzulArgumentHelpFormatter,
)
from azul.maintenance import (
MaintenanceService,
)


def parse_duration(duration: str) -> timedelta:
"""
>>> parse_duration('1d')
datetime.timedelta(days=1)
>>> parse_duration('24 hours')
datetime.timedelta(days=1)
>>> parse_duration('.5 Days 12 hours')
datetime.timedelta(days=1)
>>> parse_duration('2h20Min')
datetime.timedelta(seconds=8400)
>>> parse_duration('1 H 80m')
datetime.timedelta(seconds=8400)
>>> parse_duration('140 Minutes')
datetime.timedelta(seconds=8400)
>>> parse_duration('2 Days 3hours 4min 5 secs')
datetime.timedelta(days=2, seconds=11045)
>>> parse_duration('1d 25h')
datetime.timedelta(days=2, seconds=3600)
>>> parse_duration('1m30s')
datetime.timedelta(seconds=90)
>>> parse_duration('Bad foo')
Traceback (most recent call last):
...
azul.RequirementError: Try a duration such as "2d 6hrs", "1.5 Days", or "15m"
"""

pattern = r'(\d*\.?\d+)\s*(d|h|m|s)'
matches = re.findall(pattern, duration.lower())
require(bool(matches), 'Try a duration such as "2d 6hrs", "1.5 Days", or "15m"')
time_delta = {'days': 0, 'hours': 0, 'minutes': 0, 'seconds': 0}
for value, unit in matches:
value = float(value)
match unit:
case 'd':
time_delta['days'] += value
case 'h':
time_delta['hours'] += value
case 'm':
time_delta['minutes'] += value
case 's':
time_delta['seconds'] += value
return timedelta(**time_delta)


def main(args: list[str]):
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=AzulArgumentHelpFormatter)
subparsers = parser.add_subparsers(dest="command")
list_parser = subparsers.add_parser("list", help="List events in JSON form")
list_parser.add_argument("--all", action="store_true",
help="Include completed events")
add_parser = subparsers.add_parser("add", help="Schedule an event")
add_parser.add_argument("--start", required=True,
help="Event start time (ISO format)")
add_parser.add_argument("--duration", required=True,
help="Event duration (e.g., '1h30m', '2d')")
add_parser.add_argument("--description", required=True,
help="Event description")
add_parser.add_argument("--partial-responses", nargs="+",
help="Catalog names for partial responses")
add_parser.add_argument("--degraded-performance", nargs="+",
help="Catalog names for degraded performance")
add_parser.add_argument("--service-unavailable", nargs="+",
help="Catalog names for service unavailability")
cancel_parser = subparsers.add_parser("cancel",
help="Cancel a pending event")
cancel_parser.add_argument("--index", type=int, required=True,
help="Index of the event to cancel")
subparsers.add_parser("start", help="Activate a pending event")
subparsers.add_parser("end", help="Complete the active event")
adjust_parser = subparsers.add_parser("adjust",
help="Modify the active event")
adjust_parser.add_argument("--duration", required=True,
help="New event duration (e.g., '1h30m', '2d')")

args = parser.parse_args(args)

service = MaintenanceService()

if args.command == "list":
events = service.get_schedule
if args.all:
events = events.to_json()
else:
active = events.active_event()
active = {} if active is None else {'active': active.to_json()}
pending = events.pending_events()
pending = {'pending': list(pe.to_json() for pe in pending)}
events = active | pending
elif args.command == "add":
duration = int(parse_duration(args.duration).total_seconds())
events = service.provision_event(planned_start=args.start,
planned_duration=duration,
description=args.description,
partial=args.partial_responses,
degraded=args.degraded_performance,
unavailable=args.service_unavailable)
events = service.add_event(events)
elif args.command == "cancel":
events = service.cancel_event(args.index)
elif args.command == "start":
events = service.start_event()
elif args.command == "end":
events = service.end_event()
elif args.command == "adjust":
events = service.adjust_event(parse_duration(args.duration))
else:
assert False, 'Invalid command'
print(json.dumps(events, indent=4))


if __name__ == "__main__":
main(sys.argv[1:])
Loading

0 comments on commit 9916081

Please sign in to comment.