diff --git a/Makefile b/Makefile index 2af4ca2..e44ab37 100644 --- a/Makefile +++ b/Makefile @@ -33,11 +33,12 @@ help: ## Prints help for targets with comments @cat $(MAKEFILE_LIST) | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' lint: ## Run linter - @./venv/bin/black ./dashboard/* - @./venv/bin/black ./accounts/* - @./venv/bin/black ./dash/* - @./venv/bin/black ./support/* - @./venv/bin/black ./billing/* + @./venv/bin/black ./dashboard/ + @./venv/bin/black ./accounts/ + @./venv/bin/black ./dash/ + @./venv/bin/black ./support/ + @./venv/bin/black ./billing/ + @./venv/bin/black ./infrastructure/ migrate: ## Run migrations $(call run_migrations) diff --git a/billing/tests.py b/billing/tests.py index 90d83fd..d5a2b74 100644 --- a/billing/tests.py +++ b/billing/tests.py @@ -27,8 +27,10 @@ from payments import get_payment_model, RedirectNeeded, PaymentStatus from accounts.tests import register_util, login_util from dash.tests import create_configurations, create_instance_util +from dash.models import Instance from .models import Payment +from .utils import payment_fullfilled class BillingTest(TestCase): @@ -49,6 +51,8 @@ class BillingTest(TestCase): t=self, c=c, instance_name=instance_name, config=self.instance_config[0] ) + instance = Instance.objects.get(name=instance_name) + payment_uri = reverse("billing.invoice.generate", args=(instance_name,)) # generate invoice @@ -78,11 +82,17 @@ class BillingTest(TestCase): resp = c.get(reverse("billing.invoice.pending")) self.assertEqual(str.encode(invoice_uri) in resp.content, True) + self.assertEqual(payment_fullfilled(instance=instance), False) + # simulate payment. There's probably a better way to do this payment = get_payment_model().objects.get(paid_by=self.user) payment.status = PaymentStatus.CONFIRMED payment.save() + self.assertEqual(payment_fullfilled(instance=instance), True) + + # + # check if paid invoice is listed in paid invoice list view resp = c.get(reverse("billing.invoice.paid")) self.assertEqual(str.encode(invoice_uri) in resp.content, True) diff --git a/billing/utils.py b/billing/utils.py new file mode 100644 index 0000000..9693de7 --- /dev/null +++ b/billing/utils.py @@ -0,0 +1,33 @@ +# 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 . +from datetime import datetime, timedelta, timezone +from payments import get_payment_model, RedirectNeeded, PaymentStatus +from django.contrib.auth.models import User +from django.shortcuts import get_object_or_404 + +from dash.models import Instance + + +def payment_fullfilled(instance: Instance) -> bool: + Payment = get_payment_model() + now = datetime.now(tz=timezone.utc) + delta = now - timedelta(seconds=(60 * 60 * 24 * 30)) # one month + + payment = None + for p in Payment.objects.filter(date__gt=(delta), instance_name=instance.name): + if p.status == PaymentStatus.CONFIRMED: + return True + + return False diff --git a/billing/views.py b/billing/views.py index fa87276..1c2de6f 100644 --- a/billing/views.py +++ b/billing/views.py @@ -68,7 +68,7 @@ def generate_invoice(request, instance_name: str): delta = now - timedelta(seconds=(60 * 60 * 24 * 30)) # one month payment = None - for p in Payment.objects.filter(date__gt=(delta)): + for p in Payment.objects.filter(date__gt=(delta), instance_name=instance_name): if p.status == PaymentStatus.CONFIRMED: return HttpResponse("BAD REQUEST: Already paid", status=400) elif any([p.status == PaymentStatus.INPUT, p.status == PaymentStatus.WAITING]): diff --git a/dash/tests.py b/dash/tests.py index e0c20a6..8d02936 100644 --- a/dash/tests.py +++ b/dash/tests.py @@ -26,18 +26,18 @@ from .models import InstanceConfiguration, Instance def create_configurations(t: TestCase): t.instance_config = [ - InstanceConfiguration(name="Personal", rent=5.0, ram=0.5, cpu=1, storage=25), - InstanceConfiguration(name="Enthusiast", rent=10.0, ram=2, cpu=2, storage=50), - InstanceConfiguration( - name="Small Business", rent=20.0, ram=8, cpu=4, storage=64 + InstanceConfiguration.objects.get( + name="s1-2", rent=10, ram=2, cpu=1, storage=10 ), - InstanceConfiguration( - name="Enterprise", rent=100.0, ram=64, cpu=24, storage=1024 + InstanceConfiguration.objects.get( + name="s1-4", rent=20, ram=4, cpu=1, storage=20 + ), + InstanceConfiguration.objects.get( + name="s1-8", rent=40, ram=8, cpu=2, storage=40 ), ] for instance in t.instance_config: - instance.save() print(f"[*][init] Instance {instance.name} is saved") t.assertEqual( InstanceConfiguration.objects.filter(name=instance.name).exists(), True diff --git a/dashboard/settings.py b/dashboard/settings.py index 831c722..2f5fca9 100644 --- a/dashboard/settings.py +++ b/dashboard/settings.py @@ -47,6 +47,7 @@ INSTALLED_APPS = [ "oauth2_provider", "payments", "billing", + "infrastructure", ] MIDDLEWARE = [ @@ -177,8 +178,25 @@ HOSTEA = { # ref: https://gitea.hostea.org/Hostea/july-mvp/issues/17 "SUPPORT_REPOSITORY": "support", }, + "INFRA": { + "HOSTEA_REPO": { + # where to clone the repository + "PATH": "/srv/hostea/dashboard/infrastructure", + # Git remote URI of the repository + "REMOTE": "git@localhost:Hostea/enough.git", + # SSH key that can push to the Git repository remote mentioned above + "SSH_KEY": "/srv/hostea/deploy", + } + }, } EMAIL_CONFIG = env.email("EMAIL_URL", default="smtp://admin:password@localhost:10025") vars().update(EMAIL_CONFIG) + +try: + import dashboard.local_settings + + print("Found local_settings") +except ModuleNotFoundError: + pass diff --git a/dashboard/urls.py b/dashboard/urls.py index 12c3a14..13c4378 100644 --- a/dashboard/urls.py +++ b/dashboard/urls.py @@ -23,5 +23,6 @@ urlpatterns = [ path("admin/", admin.site.urls), path("dash/", include("dash.urls")), path("support/", include("support.urls")), + path("infra/", include("infrastructure.urls")), path("", include("accounts.urls")), ] diff --git a/docs/INSTALL.md b/docs/INSTALL.md index c58b301..c9d1c7e 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -72,3 +72,24 @@ PAYMENT_VARIANTS = { ) } ``` + +## Infrastructure(via [Enough](https://enough.community)) + +```python +HOSTEA = { +# <------snip--------> + "INFRA": { + "HOSTEA_REPO": { + # where to clone the repository + "PATH": "/srv/hostea/dashboard/infrastructure", + # Git remote URI of the repository + "REMOTE": "git@localhost:Hostea/enough.git", + # SSH key that can push to the Git repository remote mentioned above + "SSH_KEY": "/srv/hostea/deploy", + }, + }, +``` + +### References: + +https://enough-community.readthedocs.io/en/latest/services/hostea.html diff --git a/infrastructure/__init__.py b/infrastructure/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/infrastructure/admin.py b/infrastructure/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/infrastructure/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/infrastructure/apps.py b/infrastructure/apps.py new file mode 100644 index 0000000..039f2dd --- /dev/null +++ b/infrastructure/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class InfrastructureConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "infrastructure" diff --git a/infrastructure/migrations/0001_initial.py b/infrastructure/migrations/0001_initial.py new file mode 100644 index 0000000..d7a6dcc --- /dev/null +++ b/infrastructure/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# Generated by Django 4.0.3 on 2022-06-25 10:48 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("dash", "0006_auto_20220619_0800"), + ] + + operations = [ + migrations.CreateModel( + name="InstanceCreated", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("creted", models.BooleanField(default=False)), + ( + "instance", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="dash.instance" + ), + ), + ], + ), + ] diff --git a/infrastructure/migrations/0002_instancecreated_gitea_password.py b/infrastructure/migrations/0002_instancecreated_gitea_password.py new file mode 100644 index 0000000..0777dbd --- /dev/null +++ b/infrastructure/migrations/0002_instancecreated_gitea_password.py @@ -0,0 +1,20 @@ +# Generated by Django 4.0.3 on 2022-06-25 12:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("infrastructure", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="instancecreated", + name="gitea_password", + field=models.CharField( + default=None, max_length=32, verbose_name="Name of this configuration" + ), + ), + ] diff --git a/infrastructure/migrations/__init__.py b/infrastructure/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/infrastructure/models.py b/infrastructure/models.py new file mode 100644 index 0000000..a74f4ab --- /dev/null +++ b/infrastructure/models.py @@ -0,0 +1,27 @@ +# 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 . +from django.db import models + +from dash.models import Instance + + +class InstanceCreated(models.Model): + instance = models.ForeignKey(Instance, on_delete=models.PROTECT) + gitea_password = models.CharField( + "Name of this configuration", + default=None, + max_length=32, + ) + creted = models.BooleanField(default=False) diff --git a/infrastructure/templates/infrastructure/sh/hostscripts/create.sh b/infrastructure/templates/infrastructure/sh/hostscripts/create.sh new file mode 100644 index 0000000..4a4f752 --- /dev/null +++ b/infrastructure/templates/infrastructure/sh/hostscripts/create.sh @@ -0,0 +1,2 @@ +enough --domain $domain host create {{subdomain}}-host +enough --domain $domain service create --host {{subdomain}}-host gitea diff --git a/infrastructure/templates/infrastructure/sh/hostscripts/rm.sh b/infrastructure/templates/infrastructure/sh/hostscripts/rm.sh new file mode 100644 index 0000000..43a649c --- /dev/null +++ b/infrastructure/templates/infrastructure/sh/hostscripts/rm.sh @@ -0,0 +1 @@ +enough --domain $domain host delete hostea001-host diff --git a/infrastructure/templates/infrastructure/yml/backups.yml b/infrastructure/templates/infrastructure/yml/backups.yml new file mode 100644 index 0000000..540dc86 --- /dev/null +++ b/infrastructure/templates/infrastructure/yml/backups.yml @@ -0,0 +1,3 @@ +pets: + hosts: + {{ subdomain }}-host: diff --git a/infrastructure/templates/infrastructure/yml/gitea.yml b/infrastructure/templates/infrastructure/yml/gitea.yml new file mode 100644 index 0000000..fcf7aab --- /dev/null +++ b/infrastructure/templates/infrastructure/yml/gitea.yml @@ -0,0 +1,83 @@ +--- +# +####################################### +# +# Public hostname of the Gitea instance +# +# +gitea_host: "{{ subdomain }}.{{ '{' }}{{ '{' }} domain {{ '}' }}{{ '}' }}" +# +####################################### +# +# Mailer from +# +# +gitea_mailer_from: "noreply@{{ '{' }}{{ '{' }} domain {{ '}' }}{{ '}' }}" +# +####################################### +# +# SSH port of the Gitea instance +# +# +gitea_ssh_port: "22" +# +####################################### +# +# Gitea version +# +# +gitea_version: "1.16.8" +# +####################################### +# +# Admin user name +# +gitea_user: root +# +####################################### +# +# Admin user password +# +gitea_password: "{{ gitea_password }}" +# +####################################### +# +# Admin user email +# +gitea_email: "{{ gitea_email }}" +# +####################################### +# +# Unique hostname of the woodpecker server relative to {{ '{' }}{{ '{' }} domain {{ '}' }}{{ '}' }} +# +woodpecker_hostname: "{{ woodpecker_hostname }}" +# +####################################### +# +# Public hostname of the Woodpecker instance +# +woodpecker_host: "{{ '{' }}{{ '{' }} woodpecker_hostname {{ '}' }}{{ '}' }}.{{ '{' }}{{ '{' }} domain {{ '}' }}{{ '}' }}" +# +####################################### +# +# Gitea users with admin rights on woodpecker +# +woodpecker_admins: "{{ '{' }}{{ '{' }} gitea_user {{ '}' }}{{ '}' }}" +# +####################################### +# +# Woodpecker shared agent secret `openssl rand -hex 32` +# +woodpecker_agent_secret: {{ woodpecker_agent_secret }} +# +####################################### +# +# Woodpecker version +# +woodpecker_version: "v0.15.2" +# +####################################### +# +# Woodpecker max procs +# +woodpecker_max_procs: 1 diff --git a/infrastructure/templates/infrastructure/yml/provision.yml b/infrastructure/templates/infrastructure/yml/provision.yml new file mode 100644 index 0000000..ab67c1e --- /dev/null +++ b/infrastructure/templates/infrastructure/yml/provision.yml @@ -0,0 +1 @@ +openstack_flavor: {{ vm_size }} diff --git a/infrastructure/tests.py b/infrastructure/tests.py new file mode 100644 index 0000000..f6cd742 --- /dev/null +++ b/infrastructure/tests.py @@ -0,0 +1,104 @@ +# 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 . +from pathlib import Path + +from django.test import TestCase, Client, override_settings +from django.conf import settings + +from dash.models import Instance +from .utils import Infra + +from accounts.tests import register_util, login_util +from dash.tests import create_configurations, create_instance_util + + +class InfraUtilTest(TestCase): + """ + Tests billing system + """ + + def setUp(self): + self.username = "infrautil_user" + register_util(t=self, username=self.username) + create_configurations(t=self) + + @override_settings( + HOSTEA={ + "INFRA": { + "HOSTEA_REPO": { + "PATH": "/tmp/hostea/dashboard/test_path_util/repo/", + "REMOTE": "git@git.batsense.net:realaravinth/dummy-hostea-dash-test", + "SSH_KEY": "/src/atm/.ssh/aravinth", + }, + } + } + ) + def test_path_utils(self): + infra = Infra() + subdomain = "foo" + base = infra.repo_path + + self.assertEqual( + base.joinpath(f"inventory/host_vars/{subdomain}-host/"), + infra._host_vars_dir(subdomain=subdomain), + ) + + self.assertEqual( + base.joinpath(f"inventory/host_vars/{subdomain}-host/gitea.yml"), + infra._gitea_path(subdomain=subdomain), + ) + + self.assertEqual( + base.joinpath(f"inventory/host_vars/{subdomain}-host/provision.yml"), + infra._provision_path(subdomain=subdomain), + ) + + self.assertEqual( + base.joinpath(f"inventory/{subdomain}-backup.yml"), + infra._backup_path(subdomain=subdomain), + ) + + self.assertEqual( + base.joinpath(f"inventory/hosts-scripts/{subdomain}-host.sh"), + infra._hostscript_path(subdomain=subdomain), + ) + + @override_settings( + HOSTEA={ + "INFRA": { + "HOSTEA_REPO": { + "PATH": "/tmp/hostea/dashboard/test_add_vm/repo/", + "REMOTE": "git@git.batsense.net:realaravinth/dummy-hostea-dash-test", + "SSH_KEY": "/src/atm/.ssh/aravinth", + }, + } + } + ) + def test_add_vm(self): + infra = Infra() + c = Client() + login_util(self, c, "accounts.home") + subdomain = "add_vm" + + base = infra.repo_path + + create_instance_util( + t=self, c=c, instance_name=subdomain, config=self.instance_config[0] + ) + + instance = Instance.objects.get(name=subdomain) + woodpecker_agent_secret = infra.add_vm(instance=instance) + + # infra.remove_vm(instance=instance) diff --git a/infrastructure/urls.py b/infrastructure/urls.py new file mode 100644 index 0000000..b1cd20a --- /dev/null +++ b/infrastructure/urls.py @@ -0,0 +1,23 @@ +# 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 . +from django.contrib import admin +from django.urls import path, include + +from .views import create_instance, delete_instance + +urlpatterns = [ + path("create//", create_instance, name="infra.create"), + path("rm//", delete_instance, name="infra.rm"), +] diff --git a/infrastructure/utils.py b/infrastructure/utils.py new file mode 100644 index 0000000..cd47054 --- /dev/null +++ b/infrastructure/utils.py @@ -0,0 +1,220 @@ +# 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 + + +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) + + self.ssh_cmd = f"ssh -i {conf['SSH_KEY']}" + try: + self.repo = Repo(path=self.repo_path) + except InvalidGitRepositoryError: + self.repo = Repo.clone_from( + conf["REMOTE"], self.repo_path, env={"GIT_SSH_COMMAND": self.ssh_cmd} + ) + + 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 _hostscript_path(self, subdomain: str) -> Path: + """ + utility method: hostscript file for a subdomain + """ + return self.repo_path.joinpath(f"inventory/hosts-scripts/{subdomain}-host.sh") + + 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._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 add_vm(self, instance: Instance) -> str: + """ + Add new VM to infrastructure repository + + The gitea user password is returned + """ + self.repo.git.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("inventory/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} + ) + ) + + # hostscript = self.repo_path.join("inventory/hosts-scripts/{instance.name}-host.sh") + + 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() + return gitea_password + + def remove_vm(self, instance: Instance): + """ + Remove a VM from infrastructure repository + """ + self.repo.git.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) + + 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() diff --git a/infrastructure/views.py b/infrastructure/views.py new file mode 100644 index 0000000..e634e11 --- /dev/null +++ b/infrastructure/views.py @@ -0,0 +1,73 @@ +# 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 . +from django.shortcuts import render, redirect, get_object_or_404 +from django.utils.http import urlencode +from django.contrib.auth import authenticate, login, logout +from django.contrib.auth import get_user_model +from django.contrib.auth.decorators import login_required +from django.http import HttpResponse +from django.views.decorators.csrf import csrf_protect +from django.urls import reverse + +from accounts.decorators import confirm_access +from dash.models import Instance +from billing.utils import payment_fullfilled + +from .utils import Infra +from .models import InstanceCreated + + +def default_ctx(title: str, username: str): + """ + Default context for all dashboard pages + """ + return { + "title": title, + "username": username, + "open_instances": "open", + } + + +@login_required +def create_instance(request, instance_name: str): + """ + Dashboard homepage view + """ + instance = get_object_or_404(Instance, name=instance_name, owned_by=request.user) + if not payment_fullfilled(instance=instance): + return redirect(reverse("billing.invoice.generate", args=(instance_name,))) + + infra = Infra() + if not InstanceCreated.objects.filter(instance=instance).exists(): + instance = InstanceCreated.objects.create(instance=instance, created=True) + instance.save() + gitea_password = infra.add_vm(instance=instance) + instance.gitea_password = gitea_password + instance.save() + + return HttpResponse() + + +@login_required +@confirm_access +def delete_instance(request, instance_name: str): + """ + Dashboard homepage view + """ + instance = get_object_or_404(Instance, name=instance_name, owned_by=request.user) + infra = Infra() + infra.remove_vm(instance=instance) + # TODO: push isn't implemented yet + return HttpResponse() diff --git a/requirements.txt b/requirements.txt index 50a105c..b771c4a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,8 @@ django-oauth-toolkit==2.0.0 django-payments==1.0.0 django-phonenumber-field==6.3.0 djangorestframework==3.13.1 +gitdb==4.0.9 +GitPython==3.1.27 greenlet==1.1.2 idna==3.3 isort==5.10.1 @@ -34,6 +36,7 @@ pylint==2.12.2 pynvim==0.4.3 pytz==2022.1 requests==2.27.1 +smmap==5.0.0 sqlparse==0.4.2 stripe==3.4.0 tblib==1.7.0