first try at creating VirtualizationInterface
This commit is contained in:
		| @ -49,17 +49,17 @@ def login(): | ||||
|  | ||||
|                 current_app.config["FLASK_MAIL_INSTANCE"].send( | ||||
|                     Message( | ||||
|                     "Click This Link to Login to Capsul", | ||||
|                     body=f""" | ||||
|                         Navigate to {link} to log into capsul. | ||||
|                         "Click This Link to Login to Capsul", | ||||
|                         body=f""" | ||||
|                             Navigate to {link} to log into capsul. | ||||
|  | ||||
|                         If you didn't request this, ignore this message. | ||||
|                     """, | ||||
|                     html=f""" | ||||
|                         <p>Navigate to <a href="{link}">{link}</a> to log into capsul.</p> | ||||
|                         <p>If you didn't request this, ignore this message.</p> | ||||
|                     """, | ||||
|                     recipients=[email] | ||||
|                             If you didn't request this, ignore this message. | ||||
|                         """, | ||||
|                         html=f""" | ||||
|                             <p>Navigate to <a href="{link}">{link}</a> to log into capsul.</p> | ||||
|                             <p>If you didn't request this, ignore this message.</p> | ||||
|                         """, | ||||
|                         recipients=[email] | ||||
|                     ) | ||||
|                 ) | ||||
|  | ||||
|  | ||||
| @ -8,7 +8,7 @@ from psycopg2 import pool | ||||
| from flask import current_app | ||||
| from flask import g | ||||
|  | ||||
| from capsulflask.model import Model | ||||
| from capsulflask.db_model import DBModel | ||||
|  | ||||
| def init_app(app): | ||||
|   databaseUrl = urlparse(app.config['DATABASE_URL']) | ||||
| @ -111,8 +111,8 @@ def get_model(): | ||||
|     if 'model' not in g: | ||||
|         connection = current_app.config['PSYCOPG2_CONNECTION_POOL'].getconn() | ||||
|         cursor = connection.cursor() | ||||
|         g.model = Model(connection, cursor) | ||||
|     return g.model | ||||
|         g.db_model = DBModel(connection, cursor) | ||||
|     return g.db_model | ||||
|  | ||||
|  | ||||
| def close_db(e=None): | ||||
|  | ||||
							
								
								
									
										33
									
								
								capsulflask/db_model.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								capsulflask/db_model.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
|  | ||||
| from nanoid import generate | ||||
|  | ||||
|  | ||||
| class DBModel: | ||||
|   def __init__(self, connection, cursor): | ||||
|     self.connection = connection | ||||
|     self.cursor = cursor | ||||
|  | ||||
|   def login(self, email): | ||||
|     self.cursor.execute("SELECT * FROM accounts WHERE email = %s", (email, )) | ||||
|     if len(self.cursor.fetchall()) == 0: | ||||
|       self.cursor.execute("INSERT INTO accounts (email) VALUES (%s)", (email, )) | ||||
|  | ||||
|     self.cursor.execute("SELECT token FROM login_tokens WHERE email = %s", (email, )) | ||||
|     if len(self.cursor.fetchall()) > 2: | ||||
|       return None | ||||
|  | ||||
|     token = generate() | ||||
|     self.cursor.execute("INSERT INTO login_tokens (email, token) VALUES (%s, %s)", (email, token)) | ||||
|     self.connection.commit() | ||||
|  | ||||
|     return token | ||||
|      | ||||
|   def consumeToken(self, token): | ||||
|     self.cursor.execute("SELECT email FROM login_tokens WHERE token = %s", (token, )) | ||||
|     rows = self.cursor.fetchall() | ||||
|     if len(rows) > 0: | ||||
|       email = rows[0][0] | ||||
|       self.cursor.execute("DELETE FROM login_tokens WHERE email = %s", (email, )) | ||||
|       self.connection.commit() | ||||
|       return email | ||||
|     return None | ||||
| @ -1,9 +1,17 @@ | ||||
| DROP TABLE payments; | ||||
|  | ||||
| DROP TABLE logintokens; | ||||
| DROP TABLE login_tokens; | ||||
|  | ||||
| DROP TABLE vm_ssh_public_key; | ||||
|  | ||||
| DROP TABLE vms; | ||||
|  | ||||
| DROP TABLE vm_sizes; | ||||
|  | ||||
| DROP TABLE os_images; | ||||
|  | ||||
| DROP TABLE ssh_public_keys; | ||||
|  | ||||
| DROP TABLE accounts; | ||||
|  | ||||
| UPDATE schemaversion SET version = 1; | ||||
| @ -1,15 +1,48 @@ | ||||
|  | ||||
|  | ||||
| CREATE TABLE accounts ( | ||||
|    email TEXT PRIMARY KEY NOT NULL, | ||||
|    created  TIMESTAMP NOT NULL DEFAULT NOW() | ||||
|   email TEXT PRIMARY KEY NOT NULL, | ||||
|   created  TIMESTAMP NOT NULL DEFAULT NOW() | ||||
| ); | ||||
|  | ||||
| CREATE TABLE os_images ( | ||||
|   id    TEXT PRIMARY KEY NOT NULL, | ||||
|   template_image_file_name TEXT NOT NULL, | ||||
|   description TEXT NOT NULL | ||||
| ); | ||||
|  | ||||
| CREATE TABLE vm_sizes ( | ||||
|   id    TEXT PRIMARY KEY NOT NULL, | ||||
|   dollars_per_month NUMERIC(8, 2) NOT NULL, | ||||
|   memory_megabytes INTEGER NOT NULL, | ||||
|   vcpus INTEGER NOT NULL | ||||
| ); | ||||
|  | ||||
| CREATE TABLE ssh_public_keys ( | ||||
|   id    SERIAL PRIMARY KEY, | ||||
|   email TEXT REFERENCES accounts(email) ON DELETE RESTRICT, | ||||
|   name TEXT NOT NULL, | ||||
|   content TEXT NOT NULL, | ||||
|   UNIQUE (id, email) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE vms ( | ||||
|    id    TEXT PRIMARY KEY NOT NULL, | ||||
|    email TEXT REFERENCES accounts(email) ON DELETE RESTRICT, | ||||
|    created  TIMESTAMP NOT NULL DEFAULT NOW(), | ||||
|    deleted  TIMESTAMP NOT NULL | ||||
|   id    TEXT PRIMARY KEY NOT NULL, | ||||
|   email TEXT REFERENCES accounts(email) ON DELETE RESTRICT, | ||||
|   os    TEXT REFERENCES os_images(id) ON DELETE RESTRICT, | ||||
|   size  TEXT REFERENCES vm_sizes(id) ON DELETE RESTRICT, | ||||
|   created  TIMESTAMP NOT NULL DEFAULT NOW(), | ||||
|   deleted  TIMESTAMP, | ||||
|   UNIQUE (id, email) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE vm_ssh_public_key ( | ||||
|   ssh_public_key_id  INTEGER NOT NULL, | ||||
|   email              TEXT NOT NULL, | ||||
|   vm_id              TEXT NOT NULL, | ||||
|   FOREIGN KEY (email, ssh_public_key_id) REFERENCES ssh_public_keys(email, id) ON DELETE RESTRICT, | ||||
|   FOREIGN KEY (email, vm_id)             REFERENCES vms(email, id) ON DELETE RESTRICT, | ||||
|   PRIMARY KEY (email, vm_id, ssh_public_key_id) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE payments ( | ||||
| @ -19,11 +52,28 @@ CREATE TABLE payments ( | ||||
|   PRIMARY KEY (email, created) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE logintokens ( | ||||
| CREATE TABLE login_tokens ( | ||||
|   email      TEXT REFERENCES accounts(email) ON DELETE RESTRICT, | ||||
|   created    TIMESTAMP NOT NULL DEFAULT NOW(), | ||||
|   token      TEXT NOT NULL, | ||||
|   PRIMARY KEY (email, created) | ||||
| ); | ||||
|  | ||||
| INSERT INTO os_images (id, template_image_file_name, description) | ||||
| VALUES  ('debian10', 'debian-10-genericcloud-amd64-20191117-80.qcow2', 'Debian 10 (Buster)'), | ||||
|         ('centos7', 'CentOS-7-x86_64-GenericCloud.qcow2', 'CentOS 7'), | ||||
|         ('centos8', 'CentOS-8-GenericCloud-8.1.1911-20200113.3.x86_64.qcow2', 'CentOS 8'), | ||||
|         ('ubuntu18', 'ubuntu-18.04-minimal-cloudimg-amd64.img', 'Ubuntu 18.04 LTS (Bionic Beaver)'), | ||||
|         ('alpine311', 'alpine-cloud-2020-04-18.qcow2', 'Alpine Linux 3.11'), | ||||
|         ('openbsd66', 'openbsd-cloud-2020-05.qcow2', 'OpenBSD 6.6'), | ||||
|         ('guix110', 'guixsystem-cloud-2020-05.qcow2', 'Guix System 1.1.0'); | ||||
|  | ||||
| INSERT INTO vm_sizes (id, dollars_per_month, memory_megabytes, vcpus) | ||||
| VALUES  ('f1-s',    5.33,   512, 1), | ||||
|         ('f1-m',    7.16,  1024, 1), | ||||
|         ('f1-l',    8.92,  2048, 1), | ||||
|         ('f1-x',   16.16,  4096, 2), | ||||
|         ('f1-xx',  29.66,  8192, 4), | ||||
|         ('f1-xxx', 57.58, 16384, 8); | ||||
|  | ||||
| UPDATE schemaversion SET version = 2; | ||||
							
								
								
									
										79
									
								
								capsulflask/shell_scripts/create.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								capsulflask/shell_scripts/create.sh
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,79 @@ | ||||
| #!/bin/sh -e | ||||
| # | ||||
| # create VMs for the capsul service | ||||
| # developed by Cyberia Heavy Industries | ||||
| # POSIX or die | ||||
|  | ||||
| vmname="$1" | ||||
| template_file="/tank/img/$2" | ||||
| vcpus="$3" | ||||
| memory="$4" | ||||
| pubkeys="$5" | ||||
| root_voume_size="25G" | ||||
|  | ||||
| if echo "$vmname" | grep -vqE '^capsul-[a-z0-9]{10}$'; then | ||||
|   echo "vmname $vmname must match "'"^capsul-[a-z0-9]{10}$"' | ||||
|   exit 1 | ||||
| fi | ||||
|  | ||||
| if [ ! -f "$template_file" ]; then  | ||||
|   echo "template $template_file not found" | ||||
|   exit 1 | ||||
| fi | ||||
|  | ||||
| if echo "$vcpus" | grep -vqE "^[0-9]+$"; then  | ||||
|   echo "vcpus \"$vcpus\" must be an integer" | ||||
|   exit 1 | ||||
| fi | ||||
|  | ||||
| if echo "$memory" | grep -vqE "^[0-9]+$"; then  | ||||
|   echo "memory \"$memory\" must be an integer" | ||||
|   exit 1 | ||||
| fi | ||||
|  | ||||
| echo "$pubkeys" | while IFS= read -r line; do | ||||
|   if echo "$line" | grep -vqE "^(ssh|ecdsa)-[0-9A-Za-z+/_=@ -]+$"; then  | ||||
|     echo "pubkey \"$line\" must match "'"^(ssh|ecdsa)-[0-9A-Za-z+/_=@ -]+$"' | ||||
|     exit 1 | ||||
|   fi | ||||
| done | ||||
|  | ||||
| disk="/tank/vm/$vmname.qcow2" | ||||
| cdrom="/tank/vm/$vmname.iso" | ||||
| xml="/tank/vm/$vmname.xml" | ||||
|  | ||||
| if [ -f /tank/vm/$vmname.qcow2 ]; then | ||||
|     echo "Randomly generated name matched an existing VM! Odds are like one in a billion. Buy a lotto ticket." | ||||
|     exit 1 | ||||
| fi | ||||
|  | ||||
| qemu-img create -f qcow2 -b "$template_file" "$disk" | ||||
| cp /tank/config/cyberia-cloudinit.yml /tmp/cloudinit.yml | ||||
| echo "$pubkeys" | while IFS= read -r line; do | ||||
|   echo "      - $line" >> /tmp/cloudinit.yml | ||||
| done | ||||
|  | ||||
| cloud-localds "$cdrom" /tmp/cloudinit.yml | ||||
|  | ||||
| qemu-img resize "$disk" "$root_voume_size" | ||||
| virt-install \ | ||||
|     --memory "$memory" \ | ||||
|     --vcpus "$vcpus" \ | ||||
|     --name "$vmname" \ | ||||
|     --disk "$disk",bus=virtio \ | ||||
|     --disk "$cdrom",device=cdrom \ | ||||
|     --os-type Linux \ | ||||
|     --os-variant generic \ | ||||
|     --virt-type kvm \ | ||||
|     --graphics vnc,listen=127.0.0.1 \ | ||||
|     --network network=public1,filterref=clean-traffic,model=virtio \ | ||||
|     --import \ | ||||
|     --print-xml > "$xml" | ||||
|  | ||||
| # --network network=public6,filterref=clean-traffic,model=virtio | ||||
| chmod 0600 "$xml" "$disk" "$cdrom" | ||||
| virsh define "$xml" | ||||
| virsh start "$vmname" | ||||
|  | ||||
| echo "success" | ||||
|  | ||||
							
								
								
									
										48
									
								
								capsulflask/shell_scripts/destroy.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								capsulflask/shell_scripts/destroy.sh
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | ||||
| #!/bin/sh | ||||
|  | ||||
| three_dots() { | ||||
| 	printf '.' | ||||
| 	sleep 0.01 | ||||
| 	printf '.' | ||||
| 	sleep 0.01 | ||||
| 	printf '.\n' | ||||
| } | ||||
|  | ||||
| vmname="$1" | ||||
|  | ||||
| if echo "$vmname" | grep -vqE '^capsul-[a-z0-9]{10}$'; then | ||||
|   echo "vmname $vmname must match "'"^capsul-[a-z0-9]{10}$"' | ||||
|   exit 1 | ||||
| fi | ||||
|  | ||||
| echo "deleting $vmname:" | ||||
| count=$(ls /tank/vm/$vmname* 2> /dev/null | wc -l) | ||||
| if [ "$count" -gt 3 ]; then | ||||
| 	echo "too many files found, exiting cowardly:" | ||||
| 	echo "$(ls /tank/vm/$vmname*)" | ||||
| 	exit 1 | ||||
| fi | ||||
|  | ||||
| virsh list --name --all | grep -q -E "^$vmname$" | ||||
| if [ "$?" -eq 1 ]; then | ||||
| 	echo "Error: $vmname not found" | ||||
| 	exit 1 | ||||
| fi | ||||
|  | ||||
| virsh list --name --state-running | grep -q -E "^$vmname$" | ||||
| if [ "$?" -eq 0 ]; then | ||||
| 	printf '  stopping vm'; three_dots | ||||
| 	virsh destroy "$vmname" > /dev/null | ||||
| fi | ||||
|  | ||||
| printf '  undefining xml.'; three_dots | ||||
| if ! [ -f "/tank/vm/$vmname.qcow2" ]; then | ||||
| 	echo "/tank/vm/$vmname.qcow2 is not a file, exiting cowardly" | ||||
| 	exit 1 | ||||
| fi | ||||
|  | ||||
| virsh undefine "$vmname" > /dev/null | ||||
| printf '  deleting disks.' ; three_dots | ||||
| rm /tank/vm/$vmname* | ||||
|  | ||||
| echo "success" | ||||
							
								
								
									
										16
									
								
								capsulflask/shell_scripts/get.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								capsulflask/shell_scripts/get.sh
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| #!/bin/sh | ||||
|  | ||||
| vmname="$1" | ||||
|  | ||||
| if echo "$vmname" | grep -vqE '^capsul-[a-z0-9]{10}$'; then | ||||
|   echo "vmname $vmname must match "'"^capsul-[a-z0-9]{10}$"' | ||||
|   exit 1 | ||||
| fi | ||||
|  | ||||
| if virsh list --name --all | grep -vqE "^$vmname$" ; then | ||||
| 	echo "Error: $vmname not found" | ||||
| 	exit 1 | ||||
| fi | ||||
|  | ||||
| # this gets the ipv4 | ||||
| virsh domifaddr "$vmname" | awk '/vnet/ {print $4}' | cut -d'/' -f1 | ||||
							
								
								
									
										3
									
								
								capsulflask/shell_scripts/listIds.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								capsulflask/shell_scripts/listIds.sh
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| #!/bin/sh | ||||
|  | ||||
| virsh list --all | grep running | grep -v ' Id' | grep -v -- '----' | awk '{print $2}' | sort | ||||
| @ -3,7 +3,7 @@ | ||||
| <head> | ||||
| <meta charset="utf-8"> | ||||
| <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||||
| <title>{% block title %}{% endblock %}{% if self.title() %} - {% endif %}capsul</title> | ||||
| <title>{% block title %}{% endblock %}{% if self.title() %} - {% endif %}capsul💊</title> | ||||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
| <link rel="shortcut icon" href="/favicon.ico" /> | ||||
| <link rel="apple-touch-icon" href="/icon.png" /> | ||||
|  | ||||
							
								
								
									
										134
									
								
								capsulflask/virt_model.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								capsulflask/virt_model.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,134 @@ | ||||
| import subprocess | ||||
| import re | ||||
|  | ||||
| from flask import current_app | ||||
| from time import sleep | ||||
| from os.path import join | ||||
| from subprocess import run | ||||
|  | ||||
| class VirtualMachine: | ||||
|   def __init__(self, id, ipv4=None, ipv6=None): | ||||
|     self.id = id | ||||
|     self.ipv4 = ipv4 | ||||
|     self.ipv6 = ipv6 | ||||
|  | ||||
| class VirtualizationInterface: | ||||
|   def get(self, id: str) -> VirtualMachine: | ||||
|     pass | ||||
|  | ||||
|   def listIds(self) -> list: | ||||
|     pass | ||||
|  | ||||
|   def create(self, id: str, template_file_name: str, memory: int, vcpus: int, ssh_public_keys: list) -> VirtualMachine: | ||||
|     pass | ||||
|  | ||||
|   def destroy(self, id: str): | ||||
|     pass | ||||
|  | ||||
| class ShellScriptVirtualization(VirtualizationInterface): | ||||
|  | ||||
|   def validateId(self, id): | ||||
|     if not re.match(r"^capsul-[a-z0-9]{10}$", id): | ||||
|       raise ValueError(f"vm id \"{id}\" must match \"^capsul-[a-z0-9]{{10}}$\"") | ||||
|  | ||||
|   def validateCompletedProcess(self, completedProcess): | ||||
|     if completedProcess.returncode != 0: | ||||
|       raise RuntimeError(f"""{" ".join(completedProcess.args)} failed with exit code {completedProcess.returncode} | ||||
|         stdout: | ||||
|         {completedProcess.stdout} | ||||
|         stderr: | ||||
|         {completedProcess.stderr} | ||||
|       """) | ||||
|  | ||||
|   def get(self, id): | ||||
|     self.validateId(id) | ||||
|     completedProcess = run([join(current_app.root_path, 'shell_scripts/get.sh'), id], capture_output=True) | ||||
|     self.validateCompletedProcess(completedProcess) | ||||
|     lines = completedProcess.stdout.splitlines() | ||||
|  | ||||
|     if not re.match(r"^([0-9]{1,3}\.){3}[0-9]{1,3}$", lines[0]): | ||||
|       return None | ||||
|  | ||||
|     return VirtualMachine(id, ipv4=lines[0]) | ||||
|  | ||||
|   def listIds(self) -> list: | ||||
|     completedProcess = run([join(current_app.root_path, 'shell_scripts/listIds.sh')], capture_output=True) | ||||
|     self.validateCompletedProcess(completedProcess) | ||||
|     return completedProcess.stdout.splitlines() | ||||
|  | ||||
|   def create(self, id: str, template_file_name: str, vcpus: int, memory_mb: int, ssh_public_keys: list): | ||||
|     self.validateId(id) | ||||
|  | ||||
|     if not re.match(r"^[a-zA-Z0-9_.-]$", template_file_name): | ||||
|       raise ValueError(f"template_file_name \"{template_file_name}\" must match \"^[a-zA-Z0-9_.-]$\"") | ||||
|  | ||||
|     for ssh_public_key in ssh_public_keys:  | ||||
|       if not re.match(r"^(ssh|ecdsa)-[0-9A-Za-z+/_=@ -]+$", ssh_public_key): | ||||
|         raise ValueError(f"ssh_public_key \"{ssh_public_key}\" must match \"^(ssh|ecdsa)-[0-9A-Za-z+/_=@ -]+$\"") | ||||
|  | ||||
|     if vcpus < 1 or vcpus > 8: | ||||
|       raise ValueError(f"vcpus \"{vcpus}\" must match 1 <= vcpus <= 8") | ||||
|  | ||||
|     if memory_mb < 512 or memory_mb > 16384: | ||||
|       raise ValueError(f"memory_mb \"{memory_mb}\" must match 512 <= memory_mb <= 16384") | ||||
|  | ||||
|     ssh_keys_string = "\n".join(ssh_public_keys) | ||||
|  | ||||
|     completedProcess = run([ | ||||
|       join(current_app.root_path, 'shell_scripts/create.sh'), | ||||
|       id, | ||||
|       template_file_name, | ||||
|       str(vcpus), | ||||
|       str(memory_mb), | ||||
|       ssh_keys_string | ||||
|     ], capture_output=True) | ||||
|  | ||||
|     self.validateCompletedProcess(completedProcess) | ||||
|     lines = completedProcess.stdout.splitlines() | ||||
|  | ||||
|     vmSettings = f""" | ||||
|       id={id} | ||||
|       template_file_name={template_file_name} | ||||
|       vcpus={str(vcpus)} | ||||
|       memory={str(memory_mb)} | ||||
|       ssh_public_keys={ssh_keys_string} | ||||
|     """ | ||||
|  | ||||
|     if not lines[len(lines)-1] == "success": | ||||
|       raise ValueError(f"""failed to create vm with: | ||||
|         {vmSettings} | ||||
|         stdout: | ||||
|         {completedProcess.stdout} | ||||
|         stderr: | ||||
|         {completedProcess.stderr} | ||||
|       """) | ||||
|      | ||||
|     for _ in range(0, 10): | ||||
|       sleep(6) | ||||
|       result = self.get(id) | ||||
|       if result != None: | ||||
|         return result | ||||
|  | ||||
|     for _ in range(0, 10): | ||||
|       sleep(60) | ||||
|       result = self.get(id) | ||||
|       if result != None: | ||||
|         return result | ||||
|      | ||||
|     raise TimeoutError(f"""timed out waiting for vm to obtain an IP address: | ||||
|         {vmSettings} | ||||
|     """) | ||||
|  | ||||
|   def destroy(self, id: str): | ||||
|     self.validateId(id) | ||||
|     completedProcess = run([join(current_app.root_path, 'shell_scripts/destroy.sh'), id], capture_output=True) | ||||
|     self.validateCompletedProcess(completedProcess) | ||||
|     lines = completedProcess.stdout.splitlines() | ||||
|  | ||||
|     if not lines[len(lines)-1] == "success": | ||||
|       raise ValueError(f"""failed to destroy vm "{id}": | ||||
|         stdout: | ||||
|         {completedProcess.stdout} | ||||
|         stderr: | ||||
|         {completedProcess.stderr} | ||||
|       """) | ||||
		Reference in New Issue
	
	Block a user