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 21, 2024
1 parent 5a94830 commit 5c24f7a
Show file tree
Hide file tree
Showing 4 changed files with 339 additions and 9 deletions.
44 changes: 44 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 (
MaintenanceController,
)
from azul.openapi import (
application_json,
format_description as fd,
Expand Down Expand Up @@ -328,6 +331,10 @@ def manifest_controller(self) -> ManifestController:
return self._service_controller(ManifestController,
manifest_url_func=manifest_url_func)

@cached_property
def maintenance_controller(self) -> MaintenanceController:
return MaintenanceController()

def _service_controller(self, controller_cls: Type[C], **kwargs) -> C:
file_url_func: FileUrlFunc = self.file_url
return self._controller(controller_cls,
Expand Down Expand Up @@ -870,6 +877,43 @@ 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():
schedule = app.maintenance_controller.get_maintenance_schedule()
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
125 changes: 125 additions & 0 deletions scripts/manage_maintenance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""
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 sys

from more_itertools import (
one,
)

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


def parse_duration(duration: str) -> timedelta:
"""
Code which parses the duration string (e.g., "1h30m", "2d")
and return a timedelta object
"""

def _parse_duration(lapse: str, duration: str):
shorthand = lapse[0] # E.g. hours=h, minutes=m, seconds=s
duration = duration.replace(lapse, shorthand)
singular = lapse[:-1]
duration = duration.replace(singular, shorthand).split(shorthand)
if isinstance(duration, list):
if len(duration) == 2:
carry_over, duration = duration
else:
duration = one(duration)
carry_over = 0
else:
assert False, 'Try a duration such as "2d 6h", "1.5 Days", or "15m"'
return duration, float(carry_over)

duration = ''.join(duration.lower().split())
time_delta = float()
for lapse, p in (('days', 24), ('hours', 60), ('minutes', 60), ('seconds', 1)):
duration, time = _parse_duration(lapse, duration)
time_delta += time
time_delta *= p
return timedelta(seconds=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 5c24f7a

Please sign in to comment.