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 existwip-hostea-domain
parent
8baefeb413
commit
49ae2189d4
|
@ -23,10 +23,23 @@ from oauth2_provider.models import get_application_model
|
||||||
from oauth2_provider.generators import generate_client_id, generate_client_secret
|
from oauth2_provider.generators import generate_client_id, generate_client_secret
|
||||||
|
|
||||||
from dash.models import InstanceConfiguration, Instance
|
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
|
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
|
@unique
|
||||||
class Actions(Enum):
|
class Actions(Enum):
|
||||||
CREATE = "create"
|
CREATE = "create"
|
||||||
|
@ -73,22 +86,26 @@ class Command(BaseCommand):
|
||||||
flavor = options[self.flavor_key]
|
flavor = options[self.flavor_key]
|
||||||
vm_name = options[self.vm_name_key]
|
vm_name = options[self.vm_name_key]
|
||||||
|
|
||||||
if flavor == "small":
|
size = translate_sizes(flavor)
|
||||||
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
|
|
||||||
user = get_user_model().objects.get(username=owner)
|
user = get_user_model().objects.get(username=owner)
|
||||||
try:
|
try:
|
||||||
instance = create_instance(
|
instance = create_instance(
|
||||||
vm_name=vm_name, configuration_name=size, user=user
|
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("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:
|
except Exception as e:
|
||||||
self.stderr.write(self.style.ERROR(f"error: {str(e)}"))
|
self.stderr.write(self.style.ERROR(f"error: {str(e)}"))
|
||||||
|
|
||||||
|
@ -96,6 +113,7 @@ class Command(BaseCommand):
|
||||||
from infrastructure.utils import delete_vm
|
from infrastructure.utils import delete_vm
|
||||||
|
|
||||||
vm_name = options[self.vm_name_key]
|
vm_name = options[self.vm_name_key]
|
||||||
|
if Instance.objects.filter(name=vm_name).exists():
|
||||||
instance = Instance.objects.get(name=vm_name)
|
instance = Instance.objects.get(name=vm_name)
|
||||||
delete_vm(instance=instance, owner=instance.owned_by.username)
|
delete_vm(instance=instance, owner=instance.owned_by.username)
|
||||||
|
|
||||||
|
|
|
@ -29,9 +29,10 @@ from git import Repo
|
||||||
from dash.models import Instance, InstanceConfiguration
|
from dash.models import Instance, InstanceConfiguration
|
||||||
from accounts.tests import register_util, login_util
|
from accounts.tests import register_util, login_util
|
||||||
from dash.tests import create_configurations, create_instance_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 .utils import Infra
|
||||||
|
from .models import InstanceCreated
|
||||||
|
|
||||||
|
|
||||||
def custom_config(test_name: str):
|
def custom_config(test_name: str):
|
||||||
|
@ -140,9 +141,30 @@ class InfraUtilTest(TestCase):
|
||||||
|
|
||||||
self.assertEqual(instance_created.created, True)
|
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)
|
call_command("vm", "delete", vm_name)
|
||||||
out = stdout.getvalue()
|
out = stdout.getvalue()
|
||||||
|
|
||||||
self.assertEqual(Instance.objects.filter(name=vm_name).exists(), False)
|
self.assertEqual(Instance.objects.filter(name=vm_name).exists(), False)
|
||||||
host_vars_dir = infra._host_vars_dir(subdomain)
|
host_vars_dir = infra._host_vars_dir(subdomain)
|
||||||
self.assertEqual(host_vars_dir.exists(), False)
|
self.assertEqual(host_vars_dir.exists(), False)
|
||||||
|
|
||||||
|
# run delete VM command to crudely check idempotency
|
||||||
|
call_command("vm", "delete", vm_name)
|
||||||
|
|
|
@ -27,14 +27,16 @@ from dash.models import Instance
|
||||||
from .models import InstanceCreated
|
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()
|
infra = Infra()
|
||||||
if not InstanceCreated.objects.filter(instance=instance).exists():
|
if not InstanceCreated.objects.filter(instance=instance).exists():
|
||||||
gitea_password = infra.add_vm(instance=instance)
|
gitea_password = infra.add_vm(instance=instance)
|
||||||
instance = InstanceCreated.objects.create(
|
instance = InstanceCreated.objects.create(instance=instance, created=True)
|
||||||
instance=instance, gitea_password=gitea_password, created=True
|
|
||||||
)
|
|
||||||
instance.save()
|
instance.save()
|
||||||
|
return gitea_password
|
||||||
|
|
||||||
|
|
||||||
def delete_vm(instance: Instance, owner: str):
|
def delete_vm(instance: Instance, owner: str):
|
||||||
|
@ -101,8 +103,7 @@ class Infra:
|
||||||
"""
|
"""
|
||||||
utility method: hostscript file for a subdomain
|
utility method: hostscript file for a subdomain
|
||||||
"""
|
"""
|
||||||
path = self.repo_path.joinpath(f"hosts-scripts/{subdomain}-host.sh")
|
return self.repo_path.joinpath(f"hosts-scripts/{subdomain}-host.sh")
|
||||||
return path
|
|
||||||
|
|
||||||
def write_hostscript(self, subdomain: str, content: str):
|
def write_hostscript(self, subdomain: str, content: str):
|
||||||
"""
|
"""
|
||||||
|
@ -246,6 +247,8 @@ class Infra:
|
||||||
self._pull()
|
self._pull()
|
||||||
subdomain = instance.name
|
subdomain = instance.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
host_vars_dir = self._host_vars_dir(subdomain)
|
host_vars_dir = self._host_vars_dir(subdomain)
|
||||||
shutil.rmtree(host_vars_dir)
|
shutil.rmtree(host_vars_dir)
|
||||||
|
|
||||||
|
@ -254,6 +257,8 @@ class Infra:
|
||||||
|
|
||||||
service = self._service_path(subdomain)
|
service = self._service_path(subdomain)
|
||||||
os.remove(service)
|
os.remove(service)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
hostscript = self._hostscript_path(subdomain)
|
hostscript = self._hostscript_path(subdomain)
|
||||||
with open(hostscript, "w", encoding="utf-8") as f:
|
with open(hostscript, "w", encoding="utf-8") as f:
|
||||||
|
|
Loading…
Reference in New Issue