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
wip-hostea-domain
Aravinth Manivannan 2022-06-28 20:54:21 +05:30
parent 8baefeb413
commit 49ae2189d4
Signed by: realaravinth
GPG Key ID: AD9F0F08E855ED88
3 changed files with 71 additions and 26 deletions

View File

@ -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]:

View File

@ -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)

View File

@ -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: