From 18e6a1b1418d750aafadc7a96c97028f4f404c40 Mon Sep 17 00:00:00 2001 From: forest Date: Mon, 11 May 2020 01:47:14 -0500 Subject: [PATCH] starting to build console controller and views --- capsulflask/__init__.py | 7 +- capsulflask/auth.py | 4 +- capsulflask/console.py | 61 +++++++++++++++ capsulflask/db_model.py | 51 +++++++++++- .../02_up_accounts_vms_etc.sql | 15 ++-- capsulflask/static/dropdown-handle.png | Bin 0 -> 714 bytes capsulflask/static/dropdown-handle.png~ | Bin 0 -> 714 bytes capsulflask/static/style.css | 73 ++++++++++++++---- capsulflask/templates/base.html | 10 ++- capsulflask/templates/changelog.html | 4 +- capsulflask/templates/console.html | 49 ++++++++++++ capsulflask/templates/create.html | 68 ++++++++++++++++ capsulflask/templates/faq.html | 4 +- capsulflask/templates/index.html | 8 +- capsulflask/templates/login-landing.html | 4 +- capsulflask/templates/login.html | 16 ++-- capsulflask/templates/support.html | 9 +-- capsulflask/virt_model.py | 51 ++++++------ 18 files changed, 345 insertions(+), 89 deletions(-) create mode 100644 capsulflask/static/dropdown-handle.png create mode 100644 capsulflask/static/dropdown-handle.png~ create mode 100644 capsulflask/templates/console.html create mode 100644 capsulflask/templates/create.html 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 0000000000000000000000000000000000000000..7ee083586f26d24c396952bad6cf071244f46dcd GIT binary patch literal 714 zcmV;*0yX`KP)R9M69mPu$8K@f(gduASD#JHgZFM2S7;z5iT1M%WXh#&z^ zf-5-)iVLXcpq_$Y5aL2a@rek6;&O)IcSGI~$Fco7u>-gbo~2QR zVdyBOq)X3%Gik>bAXZ>C<}J#)S59q_i}3_J9}S>+W!qcSRxm)Mzrsrr38F&u2Y9@O z!{!1pYy$VlWSf*V71tf{-48n8{45VD6Zs-@>MdY&8hp=lf-roIc?djUb1>d~qxOIk z9AsW57us3;Ke7DG~v}W6C-^=NjS|4%$wo>B5w@xIYJE1X35ECTXT)CXkvSxJa0m z@C9ZkkZklZzN=({GBON~L)Qq$95uDwokI&}93DI?9bIEv~6>UfK!y{mFc*0<*mlb=lN=B6$OLK>jq3U!6Aq96mC4#h~;+ zR?ON<`6e%Up$u4=SdOYP3>%W9W-R9M69mPu$8K@f(gduASD#JHgZFM2S7;z5iT1M%WXh#&z^ zf-5-)iVLXcpq_$Y5aL2a@rek6;&O)IcSGI~$Fco7u>-gbo~2QR zVdyBOq)X3%Gik>bAXZ>C<}J#)S59q_i}3_J9}S>+W!qcSRxm)Mzrsrr38F&u2Y9@O z!{!1pYy$VlWSf*V71tf{-48n8{45VD6Zs-@>MdY&8hp=lf-roIc?djUb1>d~qxOIk z9AsW57us3;Ke7DG~v}W6C-^=NjS|4%$wo>B5w@xIYJE1X35ECTXT)CXkvSxJa0m z@C9ZkkZklZzN=({GBON~L)Qq$95uDwokI&}93DI?9bIEv~6>UfK!y{mFc*0<*mlb=lN=B6$OLK>jq3U!6Aq96mC4#h~;+ zR?ON<`6e%Up$u4=SdOYP3>%W9W- 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 %}

    -
  • Simply log in with your email address
  • +
  • Low friction: simply log in with your email address and fund your account with Credit/Debit or Cryptocurrency
  • All root disks are backed up at no charge
  • All storage is fast, local, and solid-state
  • All network connections are low latency
  • Supported by amazing volunteers from Cyberia
  • Upfront prices, no confusing billing
  • -
  • A Minnesota non-profit organization that will never exploit you
  • +
  • Operated by a Minnesota non-profit organization that will never exploit you
  • We donate a portion of our proceeds to likeminded hacker groups around the globe

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