# Copyright © 2022 Aravinth Manivannan # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import os import shutil from pathlib import Path from django.utils.crypto import get_random_string from django.template.loader import render_to_string from django.conf import settings from git import Repo from git.exc import InvalidGitRepositoryError from dash.models import Instance from .models import InstanceCreated def create_vm_if_not_exists(instance: Instance): 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.save() def delete_vm(instance: Instance, owner: str): infra = Infra() infra.remove_vm(instance=instance) if InstanceCreated.objects.filter(instance=instance).exists(): InstanceCreated.objects.get(instance=instance).delete() instance.delete() class Infra: """ Utility function to manage infrastructure repository """ def __init__(self): conf = settings.HOSTEA["INFRA"]["HOSTEA_REPO"] self.repo_path = Path(conf["PATH"]) if not self.repo_path.exists(): os.makedirs(self.repo_path) ssh_cmd = f"/usr/bin/ssh -oStrictHostKeyChecking=no -i {conf['SSH_KEY']}" self.env = {"GIT_SSH_COMMAND": ssh_cmd} try: self.repo = Repo(path=self.repo_path) except InvalidGitRepositoryError: self.repo = Repo.clone_from(conf["REMOTE"], self.repo_path, env=self.env) def _host_vars_dir(self, subdomain: str) -> Path: """ utility method: get host_vars directory for a subdomain """ return self.repo_path.joinpath(f"inventory/host_vars/{subdomain}-host/") def _provision_path(self, subdomain: str) -> Path: """ utility method: get provision file pay for a subdomain """ return self._host_vars_dir(subdomain=subdomain).joinpath("provision.yml") def _gitea_path(self, subdomain: str) -> Path: """ utility method: get gitea file for a subdomain """ return self._host_vars_dir(subdomain=subdomain).joinpath("gitea.yml") def _backup_path(self, subdomain: str) -> Path: """ utility method: get backup file for a subdomain """ return self.repo_path.joinpath(f"inventory/{subdomain}-backup.yml") def _service_path(self, subdomain: str) -> Path: """ utility method: get service file for a subdomain """ return self.repo_path.joinpath(f"inventory/{subdomain}-service.yml") def _hostscript_path(self, subdomain: str) -> Path: """ utility method: hostscript file for a subdomain """ path = self.repo_path.joinpath(f"hosts-scripts/{subdomain}-host.sh") return path def write_hostscript(self, subdomain: str, content: str): """ Write contents to hostscript. Hostscript will contain the history of all actions that have been ordered on a particular VM. So content needs to be appended to it, rather than overwritten. """ hostscript = self._hostscript_path(subdomain) with open(hostscript, "a", encoding="utf-8") as f: f.write(content) f.write("\n") def _add_files(self, subdomain: str): """ Add all relevant files of a VM """ self.repo.git.add(str(self._host_vars_dir(subdomain=subdomain))) self.repo.git.add(str(self._backup_path(subdomain=subdomain))) self.repo.git.add(str(self._service_path(subdomain=subdomain))) self.repo.git.add(str(self._hostscript_path(subdomain=subdomain))) def _commit(self, action: str, subdomain: str): """ Commit changes to a VM configuration """ self._add_files(subdomain=subdomain) self.repo.git.commit( message=f"{action} VM {subdomain}", author="Dashboard Bot ", ) def _pull(self): self.repo.git.pull(env=self.env, rebase="true") def add_vm(self, instance: Instance) -> str: """ Add new VM to infrastructure repository The gitea user password is returned """ self._pull() subdomain = instance.name host_vars_dir = self._host_vars_dir(subdomain) if not host_vars_dir.exists(): os.makedirs(host_vars_dir) hostscript_path = self.repo_path.joinpath("hosts-scripts/") if not hostscript_path.exists(): os.makedirs(hostscript_path) woodpecker_agent_secret = get_random_string(64) gitea_password = get_random_string(20) ctx = { "woodpecker_agent_secret": woodpecker_agent_secret, "woodpecker_hostname": f"{subdomain}-ci", "woodpecker_admins": f"{instance.owned_by.username}", "gitea_email": instance.owned_by.email, "gitea_password": gitea_password, "subdomain": subdomain, } gitea = self._gitea_path(subdomain) with open(gitea, "w", encoding="utf-8") as f: f.write( render_to_string( "infrastructure/yml/gitea.yml", context=ctx, ) ) # provision_template = "./templates/infrastructure/yml/provision.yml" provision = self._provision_path(subdomain) # TODO: instance config names are different the flavours expected: # ``` # openstack_flavor: ‘{{ openstack_flavor_medium }}’ * openstack_flavor: ‘{{ openstack_flavor_large }}’ # ``` # check with @dachary about this size = None if instance.configuration_id.name == "s1-2": size = "openstack_flavor_small" elif instance.configuration_id.name == "s1-4": size = "openstack_flavor_medium" elif instance.configuration_id.name == "s1-8": size = "openstack_flavor_large" else: size = instance.configuration_id.name with open(provision, "w", encoding="utf-8") as f: f.write( render_to_string( "infrastructure/yml/provision.yml", context={"vm_size": size} ) ) # backup = self.repo_path.joinpath(f"inventory/{instance.name}-backup.yml") backup = self._backup_path(subdomain) # backup_template = "./templates/infrastructure/yml/provision.yml" with open(backup, "w", encoding="utf-8") as f: f.write( render_to_string( "infrastructure/yml/backups.yml", context={"subdomain": subdomain} ) ) service = self._service_path(subdomain) with open(service, "w", encoding="utf-8") as f: f.write( render_to_string( "infrastructure/yml/service.yml", context={"subdomain": subdomain} ) ) # hostscript = self.repo_path.join("inventory/hosts-scripts/{instance.name}-host.sh") hostscript = self._hostscript_path(subdomain) with open(hostscript, "w", encoding="utf-8") as f: f.write("\n") self.write_hostscript( subdomain=subdomain, content=render_to_string( "infrastructure/sh/hostscripts/create.sh", context={"subdomain": subdomain}, ), ) self._commit(action="add", subdomain=subdomain) self.repo.git.push(env=self.env) return gitea_password def remove_vm(self, instance: Instance): """ Remove a VM from infrastructure repository """ self._pull() subdomain = instance.name host_vars_dir = self._host_vars_dir(subdomain) shutil.rmtree(host_vars_dir) backup = self._backup_path(subdomain) os.remove(backup) service = self._service_path(subdomain) os.remove(service) hostscript = self._hostscript_path(subdomain) with open(hostscript, "w", encoding="utf-8") as f: f.write("\n") self.write_hostscript( subdomain=subdomain, content=render_to_string( "infrastructure/sh/hostscripts/rm.sh", context={"subdomain": subdomain}, ), ) self._commit(action="rm", subdomain=subdomain) self.repo.git.push(env=self.env)