diff --git a/capsulflask/__init__.py b/capsulflask/__init__.py index b2f509c..36fefed 100644 --- a/capsulflask/__init__.py +++ b/capsulflask/__init__.py @@ -1,8 +1,10 @@ +import os from flask import Flask from flask_mail import Mail from flask import render_template -import os + +from capsulflask import virt_model def create_app(): app = Flask(__name__) @@ -21,6 +23,7 @@ def create_app(): ) app.config['FLASK_MAIL_INSTANCE'] = Mail(app) + app.config['VIRTUALIZATION_MODEL'] = virt_model.MockVirtualization() from capsulflask import db @@ -29,8 +32,8 @@ def create_app(): from capsulflask import auth, landing, console app.register_blueprint(landing.bp) - app.register_blueprint(auth.bp) + app.register_blueprint(console.bp) app.add_url_rule("/", endpoint="index") diff --git a/capsulflask/auth.py b/capsulflask/auth.py index c6e3727..6f0ace0 100644 --- a/capsulflask/auth.py +++ b/capsulflask/auth.py @@ -71,11 +71,11 @@ def login(): @bp.route("/magic/", methods=("GET", )) def magiclink(token): - email = get_model().consumeToken(token) + email = get_model().consume_token(token) if email is not None: session.clear() session["account"] = email - return redirect(url_for("index")) + return redirect(url_for("console.index")) else: abort(404, f"Token {token} doesn't exist or has already been used.") diff --git a/capsulflask/console.py b/capsulflask/console.py index 0b3e819..161e386 100644 --- a/capsulflask/console.py +++ b/capsulflask/console.py @@ -9,8 +9,69 @@ from flask import session from flask import render_template from flask_mail import Message from werkzeug.exceptions import abort +from nanoid import generate + +from capsulflask.auth import account_required from capsulflask.db import get_model bp = Blueprint("console", __name__, url_prefix="/console") +def makeCapsulId(): + lettersAndNumbers = generate(alphabet="1234567890qwertyuiopasdfghjklzxcvbnm", size=10) + return f"capsul-{lettersAndNumbers}" + +@bp.route("/") +@account_required +def index(): + return render_template("console.html", vms=get_model().list_vms_for_account(session["account"])) + +@bp.route("/create", methods=("GET", "POST")) +@account_required +def create(): + db_model = get_model() + vm_sizes = db_model.vm_sizes_dict() + operating_systems = db_model.operating_systems_dict() + ssh_public_keys = db_model.list_ssh_public_keys_for_account(session["account"]) + error = None + + if request.method == "POST": + size = request.form["size"] + os = request.form["os"] + if not size: + error = "Size is required" + elif size not in vm_sizes: + error = f"Invalid size {size}" + + if not os: + error = "OS is required" + elif os not in operating_systems: + error = f"Invalid os {os}" + + if error is None: + id = makeCapsulId() + db_model.create_vm( + email=session["account"], + id=id, + size=size, + os=os + ) + current_app.config["VIRTUALIZATION_MODEL"].create( + email = session["account"], + id=id, + template_image_file_name=operating_systems[os].template_image_file_name, + vcpus=vm_sizes[size].vcpus, + memory=vm_sizes[size].memory + ) + + return render_template( + "create.html", + ssh_public_keys=ssh_public_keys, + operating_systems=operating_systems, + vm_sizes=vm_sizes + ) + +@bp.route("/billing") +@account_required +def faq(): + return render_template("billing.html") \ No newline at end of file diff --git a/capsulflask/db_model.py b/capsulflask/db_model.py index 921406b..02c61fd 100644 --- a/capsulflask/db_model.py +++ b/capsulflask/db_model.py @@ -22,7 +22,7 @@ class DBModel: return token - def consumeToken(self, token): + def consume_token(self, token): self.cursor.execute("SELECT email FROM login_tokens WHERE token = %s", (token, )) rows = self.cursor.fetchall() if len(rows) > 0: @@ -32,7 +32,52 @@ class DBModel: return email return None - def allVmIds(self,): + def all_vm_ids(self,): self.cursor.execute("SELECT id FROM vms") return map(lambda x: x[0], self.cursor.fetchall()) - \ No newline at end of file + + def operating_systems_dict(self,): + self.cursor.execute("SELECT id, template_image_file_name, description FROM os_images") + + operatingSystems = dict() + for row in self.cursor.fetchall(): + operatingSystems[row[0]] = dict(template_image_file_name=row[1], description=row[2]) + + return operatingSystems + + def vm_sizes_dict(self,): + self.cursor.execute("SELECT id, dollars_per_month, vcpus, memory_mb, bandwidth_gb_per_month FROM vm_sizes") + + vmSizes = dict() + for row in self.cursor.fetchall(): + vmSizes[row[0]] = dict(dollars_per_month=row[1], vcpus=row[2], memory_mb=row[3], bandwidth_gb_per_month=row[4]) + + return vmSizes + + def list_ssh_public_keys_for_account(self, email): + self.cursor.execute("SELECT name, content FROM ssh_public_keys WHERE email = %s", (email, )) + return map( + lambda x: dict(name=x[0], content=x[1]), + self.cursor.fetchall() + ) + + def list_vms_for_account(self, email): + self.cursor.execute(""" + SELECT vms.id, vms.last_seen_ipv4, vms.last_seen_ipv6, vms.size, os_images.description, vms.created, vms.deleted + FROM vms JOIN os_images on os_images.id = vms.os + WHERE vms.email = %s""", + (email, ) + ) + return map( + lambda x: dict(id=x[0], ipv4=x[1], ipv6=x[2], size=x[3], os=x[4], created=x[5], deleted=x[6]), + self.cursor.fetchall() + ) + + def create_vm(self, email, id, size, os): + self.cursor.execute(""" + INSERT INTO vms (email, id, size, os) + VALUES (%s, %s, %s, %s) + """, + (email, id, size, os) + ) + self.connection.commit() \ No newline at end of file diff --git a/capsulflask/schema_migrations/02_up_accounts_vms_etc.sql b/capsulflask/schema_migrations/02_up_accounts_vms_etc.sql index ce90c7b..92664c4 100644 --- a/capsulflask/schema_migrations/02_up_accounts_vms_etc.sql +++ b/capsulflask/schema_migrations/02_up_accounts_vms_etc.sql @@ -20,11 +20,10 @@ CREATE TABLE vm_sizes ( ); 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) + PRIMARY KEY (email, name) ); CREATE TABLE vms ( @@ -40,12 +39,12 @@ CREATE TABLE vms ( ); CREATE TABLE vm_ssh_public_key ( - ssh_public_key_id INTEGER NOT NULL, + ssh_public_key_name TEXT 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, ssh_public_key_name) REFERENCES ssh_public_keys(email, name) ON DELETE RESTRICT, FOREIGN KEY (email, vm_id) REFERENCES vms(email, id) ON DELETE RESTRICT, - PRIMARY KEY (email, vm_id, ssh_public_key_id) + PRIMARY KEY (email, vm_id, ssh_public_key_name) ); CREATE TABLE payments ( @@ -63,11 +62,11 @@ CREATE TABLE login_tokens ( ); INSERT INTO os_images (id, template_image_file_name, description) -VALUES ('debian10', 'debian-10-genericcloud-amd64-20191117-80.qcow2', 'Debian 10 (Buster)'), +VALUES ('alpine311', 'alpine-cloud-2020-04-18.qcow2', 'Alpine Linux 3.11'), + ('ubuntu18', 'ubuntu-18.04-minimal-cloudimg-amd64.img', 'Ubuntu 18.04 LTS (Bionic Beaver)'), + ('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'); diff --git a/capsulflask/static/dropdown-handle.png b/capsulflask/static/dropdown-handle.png new file mode 100644 index 0000000..7ee0835 Binary files /dev/null and b/capsulflask/static/dropdown-handle.png differ diff --git a/capsulflask/static/dropdown-handle.png~ b/capsulflask/static/dropdown-handle.png~ new file mode 100644 index 0000000..7ee0835 Binary files /dev/null and b/capsulflask/static/dropdown-handle.png~ differ diff --git a/capsulflask/static/style.css b/capsulflask/static/style.css index 615bb0a..c7e4849 100644 --- a/capsulflask/static/style.css +++ b/capsulflask/static/style.css @@ -26,15 +26,14 @@ a:hover, a:active, a:visited { color: #b5bd68; } -nav .nav-row { +.nav-row { display: flex; justify-content: space-between; - margin: 2rem 0; } -nav .nav-row:last-child { +.nav-row:last-child { justify-content: center; } -nav .nav-row:last-child a, nav .nav-row:last-child div { +.nav-row:last-child a, .nav-row:last-child div { margin: 0 1em; } @@ -52,37 +51,77 @@ main { border: 1px dashed #bdc7b8; padding: 1rem; margin-bottom: 2em; -} -.hero { display: flex; flex-direction: column; align-items: center; } -.single-content { - margin: 2rem 0; +.full-margin { + margin: 3rem 0; +} +.half-margin { + margin: 1.5rem 0; +} +.third-margin { + margin: 1rem 0; +} + +.row { + display: flex; + align-items: center; + justify-content: space-around; + width: 100%; +} +.row.wrap { + flex-wrap: wrap; +} +.row.justify-start { + justify-content: flex-start; +} +.row.justify-end { + justify-content: flex-end; } form { display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: space-around; + flex-direction: column; + align-items: flex-start; } -input, label { +label.align { + min-width: 10em; +} + +input, select, label { margin: 0.5em; } -input { - background: none; +input, select { outline: 0; padding: 0.25em 0.5em; border-radius: 0.5em; color: #bdc7b8; } +select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + /* + re-generate the following line from the source image with: + echo "background-image: url(data:image/png;base64,$(cat capsulflask/static/dropdown-handle.png | base64 -w 0));" + */ + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA9hAAAPYQGoP6dpAAACfElEQVRYw+2WSWgVQRCGp3tmHmLEuKEEL3owguJBjBcD8eJJiCABT4IrOQiKC6jnoJ6CYBDiQkTxiQiC4nKIntR4E70JUQQ9KahIUAg6m1/FntBvXpYZE6PCFPxUTc90/dW19HuOU0op/4K8GnzcOMN8s8BCsbVZO8hCO1AzQN6EugJa7QCWguvgMB/4f5B8DeqO73vb0JEdwBetVYPnud3Yl0yU003egep3XbclCEInG8C3OE6cMIwc3/e383yXDWuniViDI5J2rXVTFEXpq9gO4Gu6GgSB43neOsyHbNwFpkK+AHWeU3dD3hDHsf06sQO4DZ6lUYVh6CilpEvPgTNpxxYgVmA15i3KuldObZGL8QQ8Hw2geWXbW9RWMECkv8JLEgmiQvQHeLyGw+YCMWwC98hkm5Q1Fdcd8d0POuD8LA8qE/kic+otYHQafM9zgjB8jXkIPGBzMN58o/aAExxkXiblP8ANsJ/9Q+mitr/gxSeUNOHVNBMjfUFJOM0KzJviACJvDPI5QgzOZsnJpKiLYLdNXpcBy1kF1WVOXKnZgDPKU8U8Ct6b5WWgh3q32yk38h2cAichr3upJmmmYyaQmiC4SJiW8AVmJ5Bs9DG+q2SCMjIMjkPcMx6HytHRUtPTYK69TnM6dPcHKSPNtTiK6kZsyNS7OpF/lXOsZEL6qO18u7Zpn2TXeJZe2gn5/cl8qwKzvRF12dR7InkDdkD+NI9fnTcAHD4yd8Wg9EBWzNpL+SYveaEMWJlYjqoyDBuSpGYyBmSEIX9XxJ/6zTt+CeoC2GwaTmrdCfnHor7UFH5oZqN6zd2+D/Lhv/FXbj1oKf/UllLKfy0/ATtM/c/kKrmhAAAAAElFTkSuQmCC); + background-repeat: no-repeat; + background-position: bottom 0.65em right 0.8em; + background-size: 0.5em; +} + +input { + background: none; +} + input[type=text] { font: calc(0.40rem + 1vmin) monospace; border: 0; @@ -91,14 +130,14 @@ input[type=text] { outline: 0; } -input[type=submit] { +input[type=submit], select { font: calc(0.40rem + 1vmin) monospace; cursor: pointer; border: 1px solid #777e73; - background: #bdc7b810; - + background-color: #bdc7b810; } + h1, h2, h3, h4, h5 { font-size:calc(0.40rem + 1vmin); margin: initial; diff --git a/capsulflask/templates/base.html b/capsulflask/templates/base.html index 6bb7b46..3d72c50 100644 --- a/capsulflask/templates/base.html +++ b/capsulflask/templates/base.html @@ -9,7 +9,7 @@ diff --git a/capsulflask/templates/changelog.html b/capsulflask/templates/changelog.html index c2b0047..bbfb449 100644 --- a/capsulflask/templates/changelog.html +++ b/capsulflask/templates/changelog.html @@ -3,9 +3,7 @@ {% block title %}Changelog{% endblock %} {% block content %} -
-

CHANGELOG

-
+

CHANGELOG

{% endblock %} {% block subcontent %}

diff --git a/capsulflask/templates/console.html b/capsulflask/templates/console.html new file mode 100644 index 0000000..3d8e1c4 --- /dev/null +++ b/capsulflask/templates/console.html @@ -0,0 +1,49 @@ +{% extends 'base.html' %} + +{% block title %}Console{% endblock %} + +{% block content %} +

+

CONSOLE

+
+
+ +
+
+ {% if vms[0] is defined %} + + + + + + + + + + + + {% for vm in vms %} + + + + + + + + + + {% endfor %} + +
idsizeipv4oscreated
{{ vm["id"] }}{{ vm["size"] }}{{ vm["ipv4"] }}{{ vm["os"] }}{{ vm["created"] }}
+ {% else %} + You don't have any Capsuls running. Create one today! + {% endif %} + +
+{% endblock %} + +{% block pagesource %}/templates/console.html{% endblock %} diff --git a/capsulflask/templates/create.html b/capsulflask/templates/create.html new file mode 100644 index 0000000..ee3fd65 --- /dev/null +++ b/capsulflask/templates/create.html @@ -0,0 +1,68 @@ +{% extends 'base.html' %} + +{% block title %}Create{% endblock %} + +{% block content %} +
+

CONSOLE - CREATE CAPSUL

+
+
+ {% if ssh_public_keys[0] is defined %} +
+CAPSUL SIZES
+============
+type     monthly   cpus  mem     ssd   net*
+-----    -------   ----  ---     ---   --- 
+f1-s     $5.33     1     512M    25G   .5TB
+f1-m     $7.16     1     1024M   25G   1TB 
+f1-l     $8.92     1     2048M   25G   2TB 
+f1-x     $16.16    2     4096M   25G   4TB 
+f1-xx    $29.66    4     8096M   25G   8TB 
+f1-xxx   $57.58    8     16G     25G   16TB
+
+* net is calculated as a per-month average
+* all VMs come standard with one public IPv4 addr
+
+
+
+ + +
+
+ + +
+
+ +
+
+ {% else %} +

You don't have any ssh public keys yet.

+

You must upload one before you can create a Capsul.

+ {% endif %} + +{% endblock %} + +{% block subcontent %} + {% if ssh_public_keys[0] is defined %} +

+ Using our services implies that you agree to our terms of service & privacy policy. +

+ + {% endif %} +{% endblock %} + +{% block pagesource %}/templates/console.html{% endblock %} diff --git a/capsulflask/templates/faq.html b/capsulflask/templates/faq.html index 4beadfa..4d547b6 100644 --- a/capsulflask/templates/faq.html +++ b/capsulflask/templates/faq.html @@ -3,9 +3,7 @@ {% block title %}FAQ{% endblock %} {% block content %} -
-

Frequently Asked Questions

-
+

Frequently Asked Questions

{% endblock %} {% block subcontent %} diff --git a/capsulflask/templates/index.html b/capsulflask/templates/index.html index 4efc257..c5ab27b 100644 --- a/capsulflask/templates/index.html +++ b/capsulflask/templates/index.html @@ -2,7 +2,6 @@ {% block content %} -

CAPSUL

        .-.  
@@ -13,20 +12,19 @@
    \   /    
     `"`     
   
- Simple, fast, private compute by https://cyberia.club -
+ Simple, fast, private compute by cyberia.club {% endblock %} {% block subcontent %}

diff --git a/capsulflask/templates/login-landing.html b/capsulflask/templates/login-landing.html index 4e10337..5a32a26 100644 --- a/capsulflask/templates/login-landing.html +++ b/capsulflask/templates/login-landing.html @@ -3,9 +3,7 @@ {% block title %}check your email{% endblock %} {% block content %} -
-
check your email. a login link has been sent to {{ email }}
-
+
Check your email. A login link has been sent to {{ email }}
{% endblock %} {% block pagesource %}/templates/login-landing.html{% endblock %} \ No newline at end of file diff --git a/capsulflask/templates/login.html b/capsulflask/templates/login.html index 242cbee..3fbe8a1 100644 --- a/capsulflask/templates/login.html +++ b/capsulflask/templates/login.html @@ -3,14 +3,16 @@ {% block title %}login{% endblock %} {% block content %} -
-
- - - +
+

LOGIN

+
+ +
+ + + +
-
- {% endblock %} {% block pagesource %}/templates/login.html{% endblock %} \ No newline at end of file diff --git a/capsulflask/templates/support.html b/capsulflask/templates/support.html index e92bd93..553af85 100644 --- a/capsulflask/templates/support.html +++ b/capsulflask/templates/support.html @@ -3,15 +3,12 @@ {% block title %}Support{% endblock %} {% block content %} -
-
+

SUPPORT

- - {% endblock %} {% block subcontent %} diff --git a/capsulflask/virt_model.py b/capsulflask/virt_model.py index 2e16d21..63776c8 100644 --- a/capsulflask/virt_model.py +++ b/capsulflask/virt_model.py @@ -5,15 +5,10 @@ from flask import current_app from time import sleep from os.path import join from subprocess import run -from nanoid import generate from capsulflask.db import get_model -def makeCapsulId(): - lettersAndNumbers = generate(alphabet="1234567890qwertyuiopasdfghjklzxcvbnm", size=10) - return f"capsul-{lettersAndNumbers}" - -def validateCapsulId(id): +def validate_capsul_id(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}}$\"") @@ -27,10 +22,10 @@ class VirtualizationInterface: def get(self, id: str) -> VirtualMachine: pass - def listIds(self) -> list: + def list_ids(self) -> list: pass - def create(self, email: str, template_file_name: str, vcpus: int, memory: int, ssh_public_keys: list) -> VirtualMachine: + def create(self, email: str, id: str, template_image_file_name: str, vcpus: int, memory: int, ssh_public_keys: list) -> VirtualMachine: pass def destroy(self, email: str, id: str): @@ -38,14 +33,14 @@ class VirtualizationInterface: class MockVirtualization(VirtualizationInterface): def get(self, id): - validateCapsulId(id) + validate_capsul_id(id) return VirtualMachine(id, ipv4="1.1.1.1") - def listIds(self) -> list: - return get_model().allVmIds() + def list_ids(self) -> list: + return get_model().all_vm_ids() - def create(self, email: str, template_file_name: str, vcpus: int, memory_mb: int, ssh_public_keys: list): - id = makeCapsulId() + def create(self, email: str, id: str, template_image_file_name: str, vcpus: int, memory_mb: int, ssh_public_keys: list): + validate_capsul_id(id) print(f"mock create: {id} for {email}") sleep(5) return VirtualMachine(id, ipv4="1.1.1.1") @@ -56,7 +51,7 @@ class MockVirtualization(VirtualizationInterface): class ShellScriptVirtualization(VirtualizationInterface): - def validateCompletedProcess(self, completedProcess, email=None): + def validate_completed_process(self, completedProcess, email=None): emailPart = "" if email != None: emailPart = f"for {email}" @@ -70,9 +65,9 @@ class ShellScriptVirtualization(VirtualizationInterface): """) def get(self, id): - validateCapsulId(id) + validate_capsul_id(id) completedProcess = run([join(current_app.root_path, 'shell_scripts/get.sh'), id], capture_output=True) - self.validateCompletedProcess(completedProcess) + self.validate_completed_process(completedProcess) lines = completedProcess.stdout.splitlines() if not re.match(r"^([0-9]{1,3}\.){3}[0-9]{1,3}$", lines[0]): @@ -80,16 +75,16 @@ class ShellScriptVirtualization(VirtualizationInterface): 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) + def list_ids(self) -> list: + completedProcess = run([join(current_app.root_path, 'shell_scripts/list_ids.sh')], capture_output=True) + self.validate_completed_process(completedProcess) return completedProcess.stdout.splitlines() - def create(self, email: str, template_file_name: str, vcpus: int, memory_mb: int, ssh_public_keys: list): - id = makeCapsulId() + def create(self, email: str, id: str, template_image_file_name: str, vcpus: int, memory_mb: int, ssh_public_keys: list): + validate_capsul_id(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_.-]$\"") + if not re.match(r"^[a-zA-Z0-9_.-]$", template_image_file_name): + raise ValueError(f"template_image_file_name \"{template_image_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): @@ -106,18 +101,18 @@ class ShellScriptVirtualization(VirtualizationInterface): completedProcess = run([ join(current_app.root_path, 'shell_scripts/create.sh'), id, - template_file_name, + template_image_file_name, str(vcpus), str(memory_mb), ssh_keys_string ], capture_output=True) - self.validateCompletedProcess(completedProcess, email) + self.validate_completed_process(completedProcess, email) lines = completedProcess.stdout.splitlines() vmSettings = f""" id={id} - template_file_name={template_file_name} + template_image_file_name={template_image_file_name} vcpus={str(vcpus)} memory={str(memory_mb)} ssh_public_keys={ssh_keys_string} @@ -149,9 +144,9 @@ class ShellScriptVirtualization(VirtualizationInterface): """) def destroy(self, email: str, id: str): - validateCapsulId(id) + validate_capsul_id(id) completedProcess = run([join(current_app.root_path, 'shell_scripts/destroy.sh'), id], capture_output=True) - self.validateCompletedProcess(completedProcess, email) + self.validate_completed_process(completedProcess, email) lines = completedProcess.stdout.splitlines() if not lines[len(lines)-1] == "success":