Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FC-20956] batou_ext.fcio: Add components that set an RG in maintenance for the time of the deployment #213

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- Added `batou_ext.fcio.Maintenance{Start,End}`: with these components it's possible to mark the RG
as "in maintenance". Components can be scheduled between these two by doing
`self.provide("needs-maintenance", self)`.
87 changes: 77 additions & 10 deletions src/batou_ext/fcio.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,19 @@ def _check_aliases(self):
return error, results


API_URL = "https://{project}:{api_key}@api.flyingcircus.io/v1"


def create_xmlrpc_client(environment: batou.environment.Environment):
rg_name = environment.overrides["provision"]["project"]
api_key = environment.overrides["provision"]["api_key"]
api_url = environment.overrides["provision"].get("api_url", API_URL)
api = xmlrpc.client.ServerProxy(
api_url.format(project=rg_name, api_key=api_key)
)
return api


class Provision(batou.component.Component):
"""FCIO provisioning component.

Expand All @@ -151,7 +164,6 @@ class Provision(batou.component.Component):
location = "rzob"
vm_environment_class = "NixOS"
vm_environment = None
api_url = "https://{project}:{api_key}@api.flyingcircus.io/v1"

# Passed by the CLI runner, not meant to be set via environment:
env_name: str = None
Expand All @@ -171,15 +183,7 @@ def load_env(self):
return environment

def get_api(self):
rg_name = self.environment_.overrides["provision"]["project"]
api_key = self.environment_.overrides["provision"]["api_key"]
api_url = self.environment_.overrides["provision"].get("api_url")
if not api_url:
api_url = self.api_url
api = xmlrpc.client.ServerProxy(
api_url.format(project=rg_name, api_key=api_key)
)
return api
return create_xmlrpc_client(self.environment_)

def get_currently_provisioned_vms(self):
return self.api.query("virtualmachine")
Expand Down Expand Up @@ -305,6 +309,69 @@ def get_diff(self, old, new):
return result


class DirectoryXMLRPC(batou.component.Component):
def configure(self):
self.xmlrpc = create_xmlrpc_client(self.environment)
self.provide("directory-xmlrpc", self.xmlrpc)


class MaintenanceStart(batou.component.Component):
def configure(self):
self.require("needs-maintenance", strict=False, reverse=True)
self.xmlrpc = self.require_one("directory-xmlrpc")

def verify(self):
raise batou.UpdateNeeded()

def update(self):
change_maintenance_state(
self.xmlrpc, self.host.name, desired_state=True
)


class MaintenanceEnd(batou.component.Component):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has the downside that setting it up is a little verbose: you have to add three additional components.
We may be able to cut it down to two by removing the DirectoryXMLRPC and setting up the client in each component.

Another idea I had was to use a decorator which injects Start end End into a component (and calls the component's configure in between), but this has the downside that the maintenance is left when the component itself is updated. I.e.

@needs_maintenance
class Foobar(Component):
  def configure(self):
    # The RG is in maintenance when X & Y are deployed
    self += X()
    self += Y()
  def update(self):
    # when this is reached, we're not in maintenance anymore.

Thoughts?

def configure(self):
self.require("needs-maintenance", strict=False)
self.xmlrpc = self.require_one("directory-xmlrpc")

def verify(self):
raise batou.UpdateNeeded()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still have bad feelings about this: every deployment with no other change will set the RG to maintenance for a short time with this.

Unfortunately I don't have a better idea after reading through batou's sources: when MaintenanceStart is deployed, we don't know yet if anything else will change later that needs maintenance. We could of course run all of the verify() methods of these components in here, but this doesn't take their sub-components into account.

Ideas? Or is this good enough?


def update(self):
change_maintenance_state(
self.xmlrpc, self.host.name, desired_state=False
)


def change_maintenance_state(
xmlrpc, host_name, desired_state, predict_only=False
):
rg_name = host_name[:-2]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I effectively assume here that each host in a deployment is in the same RG.
Do we even have cases where this is not True?

rg = next(
(rg for rg in xmlrpc.query("resourcegroup") if rg["name"] == rg_name),
None,
)
if rg is None:
raise ValueError(
f"Cannot change maintenance state of RG '{rg_name}', not in list of RGs modifyable with the xmlrpc API token."
)

if desired_state == rg["in_maintenance"]:
raise ValueError(
f"Maintenance state of RG '{rg_name}' is already '{desired_state}'!"
)

xmlrpc.apply(
[
{
"__type__": "resourcegroup",
"in_maintenance": desired_state,
"name": rg_name,
}
]
)


def main():
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
Expand Down
Loading