forked from 3wordchant/capsul-flask
starting to build console controller and views
This commit is contained in:
parent
7932db90d5
commit
18e6a1b141
@ -1,8 +1,10 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_mail import Mail
|
from flask_mail import Mail
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
import os
|
|
||||||
|
from capsulflask import virt_model
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
@ -21,6 +23,7 @@ def create_app():
|
|||||||
)
|
)
|
||||||
|
|
||||||
app.config['FLASK_MAIL_INSTANCE'] = Mail(app)
|
app.config['FLASK_MAIL_INSTANCE'] = Mail(app)
|
||||||
|
app.config['VIRTUALIZATION_MODEL'] = virt_model.MockVirtualization()
|
||||||
|
|
||||||
from capsulflask import db
|
from capsulflask import db
|
||||||
|
|
||||||
@ -29,8 +32,8 @@ def create_app():
|
|||||||
from capsulflask import auth, landing, console
|
from capsulflask import auth, landing, console
|
||||||
|
|
||||||
app.register_blueprint(landing.bp)
|
app.register_blueprint(landing.bp)
|
||||||
|
|
||||||
app.register_blueprint(auth.bp)
|
app.register_blueprint(auth.bp)
|
||||||
|
app.register_blueprint(console.bp)
|
||||||
|
|
||||||
app.add_url_rule("/", endpoint="index")
|
app.add_url_rule("/", endpoint="index")
|
||||||
|
|
||||||
|
@ -71,11 +71,11 @@ def login():
|
|||||||
|
|
||||||
@bp.route("/magic/<string:token>", methods=("GET", ))
|
@bp.route("/magic/<string:token>", methods=("GET", ))
|
||||||
def magiclink(token):
|
def magiclink(token):
|
||||||
email = get_model().consumeToken(token)
|
email = get_model().consume_token(token)
|
||||||
if email is not None:
|
if email is not None:
|
||||||
session.clear()
|
session.clear()
|
||||||
session["account"] = email
|
session["account"] = email
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("console.index"))
|
||||||
else:
|
else:
|
||||||
abort(404, f"Token {token} doesn't exist or has already been used.")
|
abort(404, f"Token {token} doesn't exist or has already been used.")
|
||||||
|
|
||||||
|
@ -9,8 +9,69 @@ from flask import session
|
|||||||
from flask import render_template
|
from flask import render_template
|
||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
from werkzeug.exceptions import abort
|
from werkzeug.exceptions import abort
|
||||||
|
from nanoid import generate
|
||||||
|
|
||||||
|
from capsulflask.auth import account_required
|
||||||
|
|
||||||
from capsulflask.db import get_model
|
from capsulflask.db import get_model
|
||||||
|
|
||||||
bp = Blueprint("console", __name__, url_prefix="/console")
|
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")
|
@ -22,7 +22,7 @@ class DBModel:
|
|||||||
|
|
||||||
return token
|
return token
|
||||||
|
|
||||||
def consumeToken(self, token):
|
def consume_token(self, token):
|
||||||
self.cursor.execute("SELECT email FROM login_tokens WHERE token = %s", (token, ))
|
self.cursor.execute("SELECT email FROM login_tokens WHERE token = %s", (token, ))
|
||||||
rows = self.cursor.fetchall()
|
rows = self.cursor.fetchall()
|
||||||
if len(rows) > 0:
|
if len(rows) > 0:
|
||||||
@ -32,7 +32,52 @@ class DBModel:
|
|||||||
return email
|
return email
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def allVmIds(self,):
|
def all_vm_ids(self,):
|
||||||
self.cursor.execute("SELECT id FROM vms")
|
self.cursor.execute("SELECT id FROM vms")
|
||||||
return map(lambda x: x[0], self.cursor.fetchall())
|
return map(lambda x: x[0], self.cursor.fetchall())
|
||||||
|
|
||||||
|
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()
|
@ -20,11 +20,10 @@ CREATE TABLE vm_sizes (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE ssh_public_keys (
|
CREATE TABLE ssh_public_keys (
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
email TEXT REFERENCES accounts(email) ON DELETE RESTRICT,
|
email TEXT REFERENCES accounts(email) ON DELETE RESTRICT,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
UNIQUE (id, email)
|
PRIMARY KEY (email, name)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE vms (
|
CREATE TABLE vms (
|
||||||
@ -40,12 +39,12 @@ CREATE TABLE vms (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE vm_ssh_public_key (
|
CREATE TABLE vm_ssh_public_key (
|
||||||
ssh_public_key_id INTEGER NOT NULL,
|
ssh_public_key_name TEXT NOT NULL,
|
||||||
email TEXT NOT NULL,
|
email TEXT NOT NULL,
|
||||||
vm_id 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,
|
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 (
|
CREATE TABLE payments (
|
||||||
@ -63,11 +62,11 @@ CREATE TABLE login_tokens (
|
|||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO os_images (id, template_image_file_name, description)
|
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'),
|
('centos7', 'CentOS-7-x86_64-GenericCloud.qcow2', 'CentOS 7'),
|
||||||
('centos8', 'CentOS-8-GenericCloud-8.1.1911-20200113.3.x86_64.qcow2', 'CentOS 8'),
|
('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'),
|
('openbsd66', 'openbsd-cloud-2020-05.qcow2', 'OpenBSD 6.6'),
|
||||||
('guix110', 'guixsystem-cloud-2020-05.qcow2', 'Guix System 1.1.0');
|
('guix110', 'guixsystem-cloud-2020-05.qcow2', 'Guix System 1.1.0');
|
||||||
|
|
||||||
|
BIN
capsulflask/static/dropdown-handle.png
Normal file
BIN
capsulflask/static/dropdown-handle.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 714 B |
BIN
capsulflask/static/dropdown-handle.png~
Normal file
BIN
capsulflask/static/dropdown-handle.png~
Normal file
Binary file not shown.
After Width: | Height: | Size: 714 B |
@ -26,15 +26,14 @@ a:hover, a:active, a:visited {
|
|||||||
color: #b5bd68;
|
color: #b5bd68;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav .nav-row {
|
.nav-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
}
|
||||||
nav .nav-row:last-child {
|
.nav-row:last-child {
|
||||||
justify-content: center;
|
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;
|
margin: 0 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,37 +51,77 @@ main {
|
|||||||
border: 1px dashed #bdc7b8;
|
border: 1px dashed #bdc7b8;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin-bottom: 2em;
|
margin-bottom: 2em;
|
||||||
}
|
|
||||||
|
|
||||||
.hero {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.single-content {
|
.full-margin {
|
||||||
margin: 2rem 0;
|
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 {
|
form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
justify-content: space-around;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input, label {
|
label.align {
|
||||||
|
min-width: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select, label {
|
||||||
margin: 0.5em;
|
margin: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input, select {
|
||||||
background: none;
|
|
||||||
outline: 0;
|
outline: 0;
|
||||||
padding: 0.25em 0.5em;
|
padding: 0.25em 0.5em;
|
||||||
border-radius: 0.5em;
|
border-radius: 0.5em;
|
||||||
color: #bdc7b8;
|
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] {
|
input[type=text] {
|
||||||
font: calc(0.40rem + 1vmin) monospace;
|
font: calc(0.40rem + 1vmin) monospace;
|
||||||
border: 0;
|
border: 0;
|
||||||
@ -91,14 +130,14 @@ input[type=text] {
|
|||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=submit] {
|
input[type=submit], select {
|
||||||
font: calc(0.40rem + 1vmin) monospace;
|
font: calc(0.40rem + 1vmin) monospace;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid #777e73;
|
border: 1px solid #777e73;
|
||||||
background: #bdc7b810;
|
background-color: #bdc7b810;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
h1, h2, h3, h4, h5 {
|
h1, h2, h3, h4, h5 {
|
||||||
font-size:calc(0.40rem + 1vmin);
|
font-size:calc(0.40rem + 1vmin);
|
||||||
margin: initial;
|
margin: initial;
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav>
|
<nav>
|
||||||
<div class="nav-row">
|
<div class="nav-row half-margin">
|
||||||
<div>
|
<div>
|
||||||
<a href="/"><b>Capsul</b></a>💊
|
<a href="/"><b>Capsul</b></a>💊
|
||||||
</div>
|
</div>
|
||||||
@ -21,9 +21,15 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-row">
|
<div class="nav-row half-margin">
|
||||||
<a href="/faq">FAQ</a>
|
<a href="/faq">FAQ</a>
|
||||||
<a href="/changelog">Changelog</a>
|
<a href="/changelog">Changelog</a>
|
||||||
|
|
||||||
|
{% if session["account"] %}
|
||||||
|
<a href="/console/">Console</a>
|
||||||
|
<a href="/console/billing">Billing</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<a href="/support">Support</a>
|
<a href="/support">Support</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -3,9 +3,7 @@
|
|||||||
{% block title %}Changelog{% endblock %}
|
{% block title %}Changelog{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="hero">
|
<div class="full-margin"><h1>CHANGELOG</h1></div>
|
||||||
<div class="single-content"><h1>CHANGELOG</h1></div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block subcontent %}
|
{% block subcontent %}
|
||||||
<p>
|
<p>
|
||||||
|
49
capsulflask/templates/console.html
Normal file
49
capsulflask/templates/console.html
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Console{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="third-margin">
|
||||||
|
<h1>CONSOLE</h1>
|
||||||
|
</div>
|
||||||
|
<div class="third-margin">
|
||||||
|
<div class="nav-row">
|
||||||
|
<a href="/console">Capsuls</a>
|
||||||
|
<a href="/console/ssh">SSH Public Keys</a>
|
||||||
|
<a href="/console/billing">Billing</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="third-margin">
|
||||||
|
{% if vms[0] is defined %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>id</th>
|
||||||
|
<th>size</th>
|
||||||
|
<th>ipv4</th>
|
||||||
|
<th>os</th>
|
||||||
|
<th>created</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for vm in vms %}
|
||||||
|
<a href="/console/{{ vm['id'] }}">
|
||||||
|
<tr>
|
||||||
|
<td>{{ vm["id"] }}</td>
|
||||||
|
<td>{{ vm["size"] }}</td>
|
||||||
|
<td>{{ vm["ipv4"] }}</td>
|
||||||
|
<td>{{ vm["os"] }}</td>
|
||||||
|
<td>{{ vm["created"] }}</td>
|
||||||
|
</tr>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
You don't have any Capsuls running. <a href="/console/create">Create one</a> today!
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block pagesource %}/templates/console.html{% endblock %}
|
68
capsulflask/templates/create.html
Normal file
68
capsulflask/templates/create.html
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Create{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="half-margin">
|
||||||
|
<h1>CONSOLE - CREATE CAPSUL</h1>
|
||||||
|
</div>
|
||||||
|
<div class="half-margin">
|
||||||
|
{% if ssh_public_keys[0] is defined %}
|
||||||
|
<pre>
|
||||||
|
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</pre>
|
||||||
|
</div>
|
||||||
|
<form method="post">
|
||||||
|
<div class="row justify-start">
|
||||||
|
<label class="align" for="size">Capsul Size</label>
|
||||||
|
<select id="size" name="size">
|
||||||
|
{% for size in vm_sizes.keys() %}<option value="{{ size }}">{{ size }}</option>{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="row justify-start">
|
||||||
|
<label class="align" for="os">Operating System</label>
|
||||||
|
<select id="os" name="os">
|
||||||
|
{% for os_id, os in operating_systems.items() %}
|
||||||
|
<option value="{{ os_id }}">{{ os.description }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="row justify-end">
|
||||||
|
<input type="submit" value="Create">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<p>You don't have any ssh public keys yet.</p>
|
||||||
|
<p>You must <a href="/console/ssh/upload">upload one</a> before you can create a Capsul.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block subcontent %}
|
||||||
|
{% if ssh_public_keys[0] is defined %}
|
||||||
|
<p>
|
||||||
|
Using our services implies that you agree to our terms of service & privacy policy.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="https://git.cyberia.club/congress/terms-of-service/plain/README">git.cyberia.club/congress/terms-of-service</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://git.cyberia.club/congress/privacy-policy/plain/README">git.cyberia.club/congress/privacy-policy</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block pagesource %}/templates/console.html{% endblock %}
|
@ -3,9 +3,7 @@
|
|||||||
{% block title %}FAQ{% endblock %}
|
{% block title %}FAQ{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="hero">
|
<div class="full-margin"><h1>Frequently Asked Questions</h1></div>
|
||||||
<div class="single-content"><h1>Frequently Asked Questions</h1></div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block subcontent %}
|
{% block subcontent %}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="hero">
|
|
||||||
<h1>CAPSUL</h1>
|
<h1>CAPSUL</h1>
|
||||||
<pre>
|
<pre>
|
||||||
.-.
|
.-.
|
||||||
@ -13,20 +12,19 @@
|
|||||||
\ /
|
\ /
|
||||||
`"`
|
`"`
|
||||||
</pre>
|
</pre>
|
||||||
<span>Simple, fast, private compute by <a href="https://cyberia.club">https://cyberia.club</a></span>
|
<span>Simple, fast, private compute by <a href="https://cyberia.club">cyberia.club</a></span>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block subcontent %}
|
{% block subcontent %}
|
||||||
<p>
|
<p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Simply log in with your email address </li>
|
<li>Low friction: simply log in with your email address and fund your account with Credit/Debit or Cryptocurrency</li>
|
||||||
<li>All root disks are backed up at no charge</li>
|
<li>All root disks are backed up at no charge</li>
|
||||||
<li>All storage is fast, local, and solid-state</li>
|
<li>All storage is fast, local, and solid-state</li>
|
||||||
<li>All network connections are low latency</li>
|
<li>All network connections are low latency</li>
|
||||||
<li>Supported by amazing volunteers from Cyberia</li>
|
<li>Supported by amazing volunteers from Cyberia</li>
|
||||||
<li>Upfront prices, no confusing billing</li>
|
<li>Upfront prices, no confusing billing</li>
|
||||||
<li>A Minnesota non-profit organization that will never exploit you</li>
|
<li>Operated by a Minnesota non-profit organization that will never exploit you</li>
|
||||||
<li>We donate a portion of our proceeds to likeminded hacker groups around the globe</li>
|
<li>We donate a portion of our proceeds to likeminded hacker groups around the globe</li>
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
|
@ -3,9 +3,7 @@
|
|||||||
{% block title %}check your email{% endblock %}
|
{% block title %}check your email{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="hero">
|
<div class="full-margin">Check your email. A login link has been sent to {{ email }}</div>
|
||||||
<div class="single-content">check your email. a login link has been sent to {{ email }}</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block pagesource %}/templates/login-landing.html{% endblock %}
|
{% block pagesource %}/templates/login-landing.html{% endblock %}
|
@ -3,14 +3,16 @@
|
|||||||
{% block title %}login{% endblock %}
|
{% block title %}login{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="hero">
|
<div class="half-margin">
|
||||||
<form method="post" class="single-content">
|
<h1>LOGIN</h1>
|
||||||
<label for="email">Email Address</label>
|
</div>
|
||||||
<input type="text" name="email" id="email" required>
|
<form method="post" class="half-margin">
|
||||||
<input type="submit" value="Log In">
|
<div class="row wrap">
|
||||||
|
<label for="email">Email Address</label>
|
||||||
|
<input type="text" name="email" id="email" required>
|
||||||
|
<input type="submit" value="Log In">
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block pagesource %}/templates/login.html{% endblock %}
|
{% block pagesource %}/templates/login.html{% endblock %}
|
@ -3,15 +3,12 @@
|
|||||||
{% block title %}Support{% endblock %}
|
{% block title %}Support{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="hero">
|
<div class="half-margin">
|
||||||
<div class="single-content">
|
|
||||||
<h1>SUPPORT</h1>
|
<h1>SUPPORT</h1>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="half-margin">
|
||||||
<div class="single-content">
|
<a href="mailto:support@cyberia.club?subject=Please%20halp!">mailto:support@cyberia.club</a>
|
||||||
<a href="mailto:support@cyberia.club?subject=Please%20halp!">Click Here to Email Support</a> (support@cyberia.club)
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block subcontent %}
|
{% block subcontent %}
|
||||||
|
@ -5,15 +5,10 @@ from flask import current_app
|
|||||||
from time import sleep
|
from time import sleep
|
||||||
from os.path import join
|
from os.path import join
|
||||||
from subprocess import run
|
from subprocess import run
|
||||||
from nanoid import generate
|
|
||||||
|
|
||||||
from capsulflask.db import get_model
|
from capsulflask.db import get_model
|
||||||
|
|
||||||
def makeCapsulId():
|
def validate_capsul_id(id):
|
||||||
lettersAndNumbers = generate(alphabet="1234567890qwertyuiopasdfghjklzxcvbnm", size=10)
|
|
||||||
return f"capsul-{lettersAndNumbers}"
|
|
||||||
|
|
||||||
def validateCapsulId(id):
|
|
||||||
if not re.match(r"^capsul-[a-z0-9]{10}$", 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}}$\"")
|
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:
|
def get(self, id: str) -> VirtualMachine:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def listIds(self) -> list:
|
def list_ids(self) -> list:
|
||||||
pass
|
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
|
pass
|
||||||
|
|
||||||
def destroy(self, email: str, id: str):
|
def destroy(self, email: str, id: str):
|
||||||
@ -38,14 +33,14 @@ class VirtualizationInterface:
|
|||||||
|
|
||||||
class MockVirtualization(VirtualizationInterface):
|
class MockVirtualization(VirtualizationInterface):
|
||||||
def get(self, id):
|
def get(self, id):
|
||||||
validateCapsulId(id)
|
validate_capsul_id(id)
|
||||||
return VirtualMachine(id, ipv4="1.1.1.1")
|
return VirtualMachine(id, ipv4="1.1.1.1")
|
||||||
|
|
||||||
def listIds(self) -> list:
|
def list_ids(self) -> list:
|
||||||
return get_model().allVmIds()
|
return get_model().all_vm_ids()
|
||||||
|
|
||||||
def create(self, email: str, template_file_name: str, vcpus: int, memory_mb: int, ssh_public_keys: list):
|
def create(self, email: str, id: str, template_image_file_name: str, vcpus: int, memory_mb: int, ssh_public_keys: list):
|
||||||
id = makeCapsulId()
|
validate_capsul_id(id)
|
||||||
print(f"mock create: {id} for {email}")
|
print(f"mock create: {id} for {email}")
|
||||||
sleep(5)
|
sleep(5)
|
||||||
return VirtualMachine(id, ipv4="1.1.1.1")
|
return VirtualMachine(id, ipv4="1.1.1.1")
|
||||||
@ -56,7 +51,7 @@ class MockVirtualization(VirtualizationInterface):
|
|||||||
|
|
||||||
class ShellScriptVirtualization(VirtualizationInterface):
|
class ShellScriptVirtualization(VirtualizationInterface):
|
||||||
|
|
||||||
def validateCompletedProcess(self, completedProcess, email=None):
|
def validate_completed_process(self, completedProcess, email=None):
|
||||||
emailPart = ""
|
emailPart = ""
|
||||||
if email != None:
|
if email != None:
|
||||||
emailPart = f"for {email}"
|
emailPart = f"for {email}"
|
||||||
@ -70,9 +65,9 @@ class ShellScriptVirtualization(VirtualizationInterface):
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
def get(self, id):
|
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)
|
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()
|
lines = completedProcess.stdout.splitlines()
|
||||||
|
|
||||||
if not re.match(r"^([0-9]{1,3}\.){3}[0-9]{1,3}$", lines[0]):
|
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])
|
return VirtualMachine(id, ipv4=lines[0])
|
||||||
|
|
||||||
def listIds(self) -> list:
|
def list_ids(self) -> list:
|
||||||
completedProcess = run([join(current_app.root_path, 'shell_scripts/listIds.sh')], capture_output=True)
|
completedProcess = run([join(current_app.root_path, 'shell_scripts/list_ids.sh')], capture_output=True)
|
||||||
self.validateCompletedProcess(completedProcess)
|
self.validate_completed_process(completedProcess)
|
||||||
return completedProcess.stdout.splitlines()
|
return completedProcess.stdout.splitlines()
|
||||||
|
|
||||||
def create(self, email: str, template_file_name: str, vcpus: int, memory_mb: int, ssh_public_keys: list):
|
def create(self, email: str, id: str, template_image_file_name: str, vcpus: int, memory_mb: int, ssh_public_keys: list):
|
||||||
id = makeCapsulId()
|
validate_capsul_id(id)
|
||||||
|
|
||||||
if not re.match(r"^[a-zA-Z0-9_.-]$", template_file_name):
|
if not re.match(r"^[a-zA-Z0-9_.-]$", template_image_file_name):
|
||||||
raise ValueError(f"template_file_name \"{template_file_name}\" must match \"^[a-zA-Z0-9_.-]$\"")
|
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:
|
for ssh_public_key in ssh_public_keys:
|
||||||
if not re.match(r"^(ssh|ecdsa)-[0-9A-Za-z+/_=@ -]+$", ssh_public_key):
|
if not re.match(r"^(ssh|ecdsa)-[0-9A-Za-z+/_=@ -]+$", ssh_public_key):
|
||||||
@ -106,18 +101,18 @@ class ShellScriptVirtualization(VirtualizationInterface):
|
|||||||
completedProcess = run([
|
completedProcess = run([
|
||||||
join(current_app.root_path, 'shell_scripts/create.sh'),
|
join(current_app.root_path, 'shell_scripts/create.sh'),
|
||||||
id,
|
id,
|
||||||
template_file_name,
|
template_image_file_name,
|
||||||
str(vcpus),
|
str(vcpus),
|
||||||
str(memory_mb),
|
str(memory_mb),
|
||||||
ssh_keys_string
|
ssh_keys_string
|
||||||
], capture_output=True)
|
], capture_output=True)
|
||||||
|
|
||||||
self.validateCompletedProcess(completedProcess, email)
|
self.validate_completed_process(completedProcess, email)
|
||||||
lines = completedProcess.stdout.splitlines()
|
lines = completedProcess.stdout.splitlines()
|
||||||
|
|
||||||
vmSettings = f"""
|
vmSettings = f"""
|
||||||
id={id}
|
id={id}
|
||||||
template_file_name={template_file_name}
|
template_image_file_name={template_image_file_name}
|
||||||
vcpus={str(vcpus)}
|
vcpus={str(vcpus)}
|
||||||
memory={str(memory_mb)}
|
memory={str(memory_mb)}
|
||||||
ssh_public_keys={ssh_keys_string}
|
ssh_public_keys={ssh_keys_string}
|
||||||
@ -149,9 +144,9 @@ class ShellScriptVirtualization(VirtualizationInterface):
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
def destroy(self, email: str, id: str):
|
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)
|
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()
|
lines = completedProcess.stdout.splitlines()
|
||||||
|
|
||||||
if not lines[len(lines)-1] == "success":
|
if not lines[len(lines)-1] == "success":
|
||||||
|
Loading…
Reference in New Issue
Block a user