first try at creating VirtualizationInterface

This commit is contained in:
forest 2020-05-10 18:59:30 -05:00
parent 7fe0d9a9c5
commit 426fad7b10
12 changed files with 395 additions and 22 deletions

View File

@ -49,17 +49,17 @@ def login():
current_app.config["FLASK_MAIL_INSTANCE"].send( current_app.config["FLASK_MAIL_INSTANCE"].send(
Message( Message(
"Click This Link to Login to Capsul", "Click This Link to Login to Capsul",
body=f""" body=f"""
Navigate to {link} to log into capsul. Navigate to {link} to log into capsul.
If you didn't request this, ignore this message. If you didn't request this, ignore this message.
""", """,
html=f""" html=f"""
<p>Navigate to <a href="{link}">{link}</a> to log into capsul.</p> <p>Navigate to <a href="{link}">{link}</a> to log into capsul.</p>
<p>If you didn't request this, ignore this message.</p> <p>If you didn't request this, ignore this message.</p>
""", """,
recipients=[email] recipients=[email]
) )
) )

View File

@ -8,7 +8,7 @@ from psycopg2 import pool
from flask import current_app from flask import current_app
from flask import g from flask import g
from capsulflask.model import Model from capsulflask.db_model import DBModel
def init_app(app): def init_app(app):
databaseUrl = urlparse(app.config['DATABASE_URL']) databaseUrl = urlparse(app.config['DATABASE_URL'])
@ -111,8 +111,8 @@ def get_model():
if 'model' not in g: if 'model' not in g:
connection = current_app.config['PSYCOPG2_CONNECTION_POOL'].getconn() connection = current_app.config['PSYCOPG2_CONNECTION_POOL'].getconn()
cursor = connection.cursor() cursor = connection.cursor()
g.model = Model(connection, cursor) g.db_model = DBModel(connection, cursor)
return g.model return g.db_model
def close_db(e=None): def close_db(e=None):

33
capsulflask/db_model.py Normal file
View 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

View File

@ -1,9 +1,17 @@
DROP TABLE payments; DROP TABLE payments;
DROP TABLE logintokens; DROP TABLE login_tokens;
DROP TABLE vm_ssh_public_key;
DROP TABLE vms; DROP TABLE vms;
DROP TABLE vm_sizes;
DROP TABLE os_images;
DROP TABLE ssh_public_keys;
DROP TABLE accounts; DROP TABLE accounts;
UPDATE schemaversion SET version = 1; UPDATE schemaversion SET version = 1;

View File

@ -1,15 +1,48 @@
CREATE TABLE accounts ( CREATE TABLE accounts (
email TEXT PRIMARY KEY NOT NULL, email TEXT PRIMARY KEY NOT NULL,
created TIMESTAMP NOT NULL DEFAULT NOW() 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 ( CREATE TABLE vms (
id TEXT PRIMARY KEY NOT NULL, id TEXT PRIMARY KEY NOT NULL,
email TEXT REFERENCES accounts(email) ON DELETE RESTRICT, email TEXT REFERENCES accounts(email) ON DELETE RESTRICT,
created TIMESTAMP NOT NULL DEFAULT NOW(), os TEXT REFERENCES os_images(id) ON DELETE RESTRICT,
deleted TIMESTAMP NOT NULL 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 ( CREATE TABLE payments (
@ -19,11 +52,28 @@ CREATE TABLE payments (
PRIMARY KEY (email, created) PRIMARY KEY (email, created)
); );
CREATE TABLE logintokens ( CREATE TABLE login_tokens (
email TEXT REFERENCES accounts(email) ON DELETE RESTRICT, email TEXT REFERENCES accounts(email) ON DELETE RESTRICT,
created TIMESTAMP NOT NULL DEFAULT NOW(), created TIMESTAMP NOT NULL DEFAULT NOW(),
token TEXT NOT NULL, token TEXT NOT NULL,
PRIMARY KEY (email, created) 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; UPDATE schemaversion SET version = 2;

View 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"

View 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"

View 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

View File

@ -0,0 +1,3 @@
#!/bin/sh
virsh list --all | grep running | grep -v ' Id' | grep -v -- '----' | awk '{print $2}' | sort

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <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"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/favicon.ico" /> <link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/icon.png" /> <link rel="apple-touch-icon" href="/icon.png" />

134
capsulflask/virt_model.py Normal file
View 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}
""")

2
test Normal file
View File

@ -0,0 +1,2 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKD3XzZTbTteIgnaFY+fiiOl9EnNN+twyNchnWjCkYqv forest@tower
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKD3XzZTbTteIgnaFY+fiiOl9EnNN+twyNchnWjCkYqv forest@tower