From 49ae2189d442d633f33aa3dd4b25b5d5c46fd433 Mon Sep 17 00:00:00 2001 From: realaravinth Date: Tue, 28 Jun 2022 20:54:21 +0530 Subject: [PATCH] feat & fix: make vm create/rm commands idempotent SUMMARY Commands are now tolerant to being invoked twice. Command: vm create Doesn't fail if VM of same name exists with the same configuration Doesn't fail if VM of the same name and different configuration exist. Updates configuration and deploys(pushes to Hostea/fleet repository) new configuration. Command: vm delete Doesn't fail if VM of given name doesn't exist --- infrastructure/management/commands/vm.py | 44 +++++++++++++++++------- infrastructure/tests.py | 24 ++++++++++++- infrastructure/utils.py | 29 +++++++++------- 3 files changed, 71 insertions(+), 26 deletions(-) diff --git a/infrastructure/management/commands/vm.py b/infrastructure/management/commands/vm.py index 540e53d..f92ac94 100644 --- a/infrastructure/management/commands/vm.py +++ b/infrastructure/management/commands/vm.py @@ -23,10 +23,23 @@ from oauth2_provider.models import get_application_model from oauth2_provider.generators import generate_client_id, generate_client_secret from dash.models import InstanceConfiguration, Instance -from dash.utils import create_instance +from dash.utils import create_instance, VmException, VmErrors from infrastructure.utils import create_vm_if_not_exists +def translate_sizes(flavor: str): + if flavor == "small": + size = "s1-2" + elif flavor == "medium": + size = "s1-4" + elif flavor == "large": + size = "s1-8" + else: + print("flavour no match") + size = flavor + return size + + @unique class Actions(Enum): CREATE = "create" @@ -73,22 +86,26 @@ class Command(BaseCommand): flavor = options[self.flavor_key] vm_name = options[self.vm_name_key] - if flavor == "small": - size = "s1-2" - elif flavor == "medium": - size = "s1-4" - elif flavor == "large": - size = "s1-8" - else: - self.stdout.write(self.style.WARN("flavour no match")) - size = flavor + size = translate_sizes(flavor) + user = get_user_model().objects.get(username=owner) try: instance = create_instance( vm_name=vm_name, configuration_name=size, user=user ) - create_vm_if_not_exists(instance) + gitea_password = create_vm_if_not_exists(instance) print("Instance created") + print(f"Gitea admin password: {gitea_password}") + except VmException as e: + if e.code == VmErrors.NAME_EXISTS: + instance = Instance.objects.get(name=vm_name) + if instance.configuration_id.name != size: + instance.configuration_id = InstanceConfiguration.objects.get( + name=size + ) + instance.save() + else: + self.stderr.write(self.style.ERROR(f"error: {str(e)}")) except Exception as e: self.stderr.write(self.style.ERROR(f"error: {str(e)}")) @@ -96,8 +113,9 @@ class Command(BaseCommand): from infrastructure.utils import delete_vm vm_name = options[self.vm_name_key] - instance = Instance.objects.get(name=vm_name) - delete_vm(instance=instance, owner=instance.owned_by.username) + if Instance.objects.filter(name=vm_name).exists(): + instance = Instance.objects.get(name=vm_name) + delete_vm(instance=instance, owner=instance.owned_by.username) def handle(self, *args, **options): for i in [self.action_key, self.vm_name_key]: diff --git a/infrastructure/tests.py b/infrastructure/tests.py index f4a748e..d3320d2 100644 --- a/infrastructure/tests.py +++ b/infrastructure/tests.py @@ -29,9 +29,10 @@ from git import Repo from dash.models import Instance, InstanceConfiguration from accounts.tests import register_util, login_util from dash.tests import create_configurations, create_instance_util -from infrastructure.models import InstanceCreated +from infrastructure.management.commands.vm import translate_sizes from .utils import Infra +from .models import InstanceCreated def custom_config(test_name: str): @@ -140,9 +141,30 @@ class InfraUtilTest(TestCase): self.assertEqual(instance_created.created, True) + # run create vm command again with same configuration to crudely check idempotency + call_command( + "vm", "create", vm_name, f"--owner={self.username}", "--flavor=medium" + ) + + # run create vm command again with different configuration but same name + # to crudely check idempotency + old_size = instance.configuration_id + call_command( + "vm", "create", vm_name, f"--owner={self.username}", "--flavor=large" + ) + instance.refresh_from_db() + self.assertEqual( + str.strip(instance.configuration_id.name) + == str.strip(translate_sizes("large")), + True, + ) + call_command("vm", "delete", vm_name) out = stdout.getvalue() self.assertEqual(Instance.objects.filter(name=vm_name).exists(), False) host_vars_dir = infra._host_vars_dir(subdomain) self.assertEqual(host_vars_dir.exists(), False) + + # run delete VM command to crudely check idempotency + call_command("vm", "delete", vm_name) diff --git a/infrastructure/utils.py b/infrastructure/utils.py index 301e210..79db0b3 100644 --- a/infrastructure/utils.py +++ b/infrastructure/utils.py @@ -27,14 +27,16 @@ from dash.models import Instance from .models import InstanceCreated -def create_vm_if_not_exists(instance: Instance): +def create_vm_if_not_exists(instance: Instance) -> str: + """ + Create VM utility. Gitea password is returned + """ infra = Infra() if not InstanceCreated.objects.filter(instance=instance).exists(): gitea_password = infra.add_vm(instance=instance) - instance = InstanceCreated.objects.create( - instance=instance, gitea_password=gitea_password, created=True - ) + instance = InstanceCreated.objects.create(instance=instance, created=True) instance.save() + return gitea_password def delete_vm(instance: Instance, owner: str): @@ -101,8 +103,7 @@ class Infra: """ utility method: hostscript file for a subdomain """ - path = self.repo_path.joinpath(f"hosts-scripts/{subdomain}-host.sh") - return path + return self.repo_path.joinpath(f"hosts-scripts/{subdomain}-host.sh") def write_hostscript(self, subdomain: str, content: str): """ @@ -246,14 +247,18 @@ class Infra: self._pull() subdomain = instance.name - host_vars_dir = self._host_vars_dir(subdomain) - shutil.rmtree(host_vars_dir) + try: - backup = self._backup_path(subdomain) - os.remove(backup) + host_vars_dir = self._host_vars_dir(subdomain) + shutil.rmtree(host_vars_dir) - service = self._service_path(subdomain) - os.remove(service) + backup = self._backup_path(subdomain) + os.remove(backup) + + service = self._service_path(subdomain) + os.remove(service) + except FileNotFoundError: + pass hostscript = self._hostscript_path(subdomain) with open(hostscript, "w", encoding="utf-8") as f: