create capsul and capsuls list page working
This commit is contained in:
parent
452f236c6b
commit
231d1ed7c0
@ -32,6 +32,12 @@ Run the app
|
|||||||
FLASK_APP=capsulflask flask run
|
FLASK_APP=capsulflask flask run
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Run the app in gunicorn locally
|
||||||
|
```
|
||||||
|
pip install gunicorn
|
||||||
|
.venv/bin/gunicorn --bind 127.0.0.1:5000 capsulflask:app
|
||||||
|
```
|
||||||
|
|
||||||
## postgres database schema management
|
## postgres database schema management
|
||||||
|
|
||||||
capsulflask has a concept of a schema version. When the application starts, it will query the database for a table named
|
capsulflask has a concept of a schema version. When the application starts, it will query the database for a table named
|
||||||
|
@ -6,36 +6,35 @@ from flask import render_template
|
|||||||
|
|
||||||
from capsulflask import virt_model
|
from capsulflask import virt_model
|
||||||
|
|
||||||
def create_app():
|
app = Flask(__name__)
|
||||||
app = Flask(__name__)
|
app.config.from_mapping(
|
||||||
app.config.from_mapping(
|
BASE_URL=os.environ.get("BASE_URL", default="http://localhost:5000"),
|
||||||
BASE_URL=os.environ.get("BASE_URL", default="http://localhost:5000"),
|
SECRET_KEY=os.environ.get("SECRET_KEY", default="dev"),
|
||||||
SECRET_KEY=os.environ.get("SECRET_KEY", default="dev"),
|
DATABASE_URL=os.environ.get("DATABASE_URL", default="sql://postgres:dev@localhost:5432/postgres"),
|
||||||
DATABASE_URL=os.environ.get("DATABASE_URL", default="sql://postgres:dev@localhost:5432/postgres"),
|
DATABASE_SCHEMA=os.environ.get("DATABASE_SCHEMA", default="public"),
|
||||||
DATABASE_SCHEMA=os.environ.get("DATABASE_SCHEMA", default="public"),
|
|
||||||
|
|
||||||
MAIL_SERVER=os.environ.get("MAIL_SERVER", default="m1.nullhex.com"),
|
MAIL_SERVER=os.environ.get("MAIL_SERVER", default="m1.nullhex.com"),
|
||||||
MAIL_PORT=os.environ.get("MAIL_PORT", default="587"),
|
MAIL_PORT=os.environ.get("MAIL_PORT", default="587"),
|
||||||
MAIL_USE_TLS=os.environ.get("MAIL_USE_TLS", default="True").lower() in ['true', '1', 't', 'y', 'yes'],
|
MAIL_USE_TLS=os.environ.get("MAIL_USE_TLS", default="True").lower() in ['true', '1', 't', 'y', 'yes'],
|
||||||
MAIL_USERNAME=os.environ.get("MAIL_USERNAME", default="forest@nullhex.com"),
|
MAIL_USERNAME=os.environ.get("MAIL_USERNAME", default="forest@nullhex.com"),
|
||||||
MAIL_PASSWORD=os.environ.get("MAIL_PASSWORD", default=""),
|
MAIL_PASSWORD=os.environ.get("MAIL_PASSWORD", default=""),
|
||||||
MAIL_DEFAULT_SENDER=os.environ.get("MAIL_DEFAULT_SENDER", default="forest@nullhex.com"),
|
MAIL_DEFAULT_SENDER=os.environ.get("MAIL_DEFAULT_SENDER", default="forest@nullhex.com"),
|
||||||
)
|
)
|
||||||
|
|
||||||
app.config['FLASK_MAIL_INSTANCE'] = Mail(app)
|
app.config['FLASK_MAIL_INSTANCE'] = Mail(app)
|
||||||
app.config['VIRTUALIZATION_MODEL'] = virt_model.MockVirtualization()
|
app.config['VIRTUALIZATION_MODEL'] = virt_model.MockVirtualization()
|
||||||
|
|
||||||
from capsulflask import db
|
from capsulflask import db
|
||||||
|
|
||||||
db.init_app(app)
|
db.init_app(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.register_blueprint(console.bp)
|
||||||
|
|
||||||
app.add_url_rule("/", endpoint="index")
|
app.add_url_rule("/", endpoint="index")
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host='127.0.0.1')
|
||||||
|
@ -33,17 +33,17 @@ def account_required(view):
|
|||||||
def login():
|
def login():
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
email = request.form["email"]
|
email = request.form["email"]
|
||||||
error = None
|
errors = list()
|
||||||
|
|
||||||
if not email:
|
if not email:
|
||||||
error = "email is required"
|
errors.append("email is required")
|
||||||
elif len(email.strip()) < 6 or email.count('@') != 1 or email.count('.') == 0:
|
elif len(email.strip()) < 6 or email.count('@') != 1 or email.count('.') == 0:
|
||||||
error = "enter a valid email address"
|
errors.append("enter a valid email address")
|
||||||
|
|
||||||
if error is None:
|
if len(errors) == 0:
|
||||||
token = get_model().login(email)
|
token = get_model().login(email)
|
||||||
if token is None:
|
if token is None:
|
||||||
error = "too many logins. please use one of the existing login links that have been emailed to you"
|
errors.append("too many logins. please use one of the existing login links that have been emailed to you")
|
||||||
else:
|
else:
|
||||||
link = f"{current_app.config['BASE_URL']}/auth/magic/{token}"
|
link = f"{current_app.config['BASE_URL']}/auth/magic/{token}"
|
||||||
|
|
||||||
@ -65,7 +65,8 @@ def login():
|
|||||||
|
|
||||||
return render_template("login-landing.html", email=email)
|
return render_template("login-landing.html", email=email)
|
||||||
|
|
||||||
flash(error)
|
for error in errors:
|
||||||
|
flash(error)
|
||||||
|
|
||||||
return render_template("login.html")
|
return render_template("login.html")
|
||||||
|
|
||||||
|
@ -25,62 +25,63 @@ def makeCapsulId():
|
|||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
@account_required
|
@account_required
|
||||||
def index():
|
def index():
|
||||||
return render_template("capsuls.html", vms=get_model().list_vms_for_account(session["account"]))
|
vms = list(map(
|
||||||
|
lambda x: dict(
|
||||||
|
id=x['id'],
|
||||||
|
size=x['size'],
|
||||||
|
ipv4=(x['ipv4'] if x['ipv4'] else "..booting.."),
|
||||||
|
ipv4_status=("ok" if x['ipv4'] else "waiting-pulse"),
|
||||||
|
os=x['os'],
|
||||||
|
created=x['created'].strftime("%b %d %Y %H:%M")
|
||||||
|
),
|
||||||
|
get_model().list_vms_for_account(session["account"])
|
||||||
|
))
|
||||||
|
return render_template("capsuls.html", vms=vms, has_vms=len(vms) > 0)
|
||||||
|
|
||||||
@bp.route("/ssh", methods=("GET", "POST"))
|
@bp.route("/ssh", methods=("GET", "POST"))
|
||||||
@account_required
|
@account_required
|
||||||
def ssh_public_keys():
|
def ssh_public_keys():
|
||||||
db_model = get_model()
|
db_model = get_model()
|
||||||
error = None
|
errors = list()
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
method = request.form["method"]
|
method = request.form["method"]
|
||||||
|
|
||||||
name = request.form["name"]
|
name = request.form["name"]
|
||||||
if not name or len(name.strip()) < 1:
|
if not name or len(name.strip()) < 1:
|
||||||
error = "Name is required"
|
errors.append("Name is required")
|
||||||
elif not re.match(r"^[0-9A-Za-z_ -]+$", name):
|
elif not re.match(r"^[0-9A-Za-z_ -]+$", name):
|
||||||
error = "Name must match \"^[0-9A-Za-z_ -]+$\""
|
errors.append("Name must match \"^[0-9A-Za-z_ -]+$\"")
|
||||||
|
|
||||||
if method == "POST":
|
if method == "POST":
|
||||||
content = request.form["content"]
|
content = request.form["content"]
|
||||||
if not content or len(content.strip()) < 1:
|
if not content or len(content.strip()) < 1:
|
||||||
error = "Content is required"
|
errors.append("Content is required")
|
||||||
else:
|
else:
|
||||||
content = content.replace("\r", "").replace("\n", "")
|
content = content.replace("\r", "").replace("\n", "")
|
||||||
if not re.match(r"^(ssh|ecdsa)-[0-9A-Za-z+/_=@ -]+$", content):
|
if not re.match(r"^(ssh|ecdsa)-[0-9A-Za-z+/_=@ -]+$", content):
|
||||||
error = "Content must match \"^(ssh|ecdsa)-[0-9A-Za-z+/_=@ -]+$\""
|
errors.append("Content must match \"^(ssh|ecdsa)-[0-9A-Za-z+/_=@ -]+$\"")
|
||||||
|
|
||||||
if db_model.ssh_public_key_name_exists(session["account"], name):
|
if db_model.ssh_public_key_name_exists(session["account"], name):
|
||||||
error = "A key with that name already exists"
|
errors.append("A key with that name already exists")
|
||||||
|
|
||||||
if error is None:
|
if len(errors) == 0:
|
||||||
db_model.create_ssh_public_key(session["account"], name, content)
|
db_model.create_ssh_public_key(session["account"], name, content)
|
||||||
|
|
||||||
elif method == "DELETE":
|
elif method == "DELETE":
|
||||||
|
|
||||||
if error is None:
|
if len(errors) == 0:
|
||||||
db_model.delete_ssh_public_key(session["account"], name)
|
db_model.delete_ssh_public_key(session["account"], name)
|
||||||
|
|
||||||
if error:
|
for error in errors:
|
||||||
flash(error)
|
flash(error)
|
||||||
|
|
||||||
# keys_list=
|
keys_list=list(map(
|
||||||
# for key in keys_list:
|
lambda x: dict(name=x['name'], content=f"{x['content'][:20]}...{x['content'][len(x['content'])-20:]}"),
|
||||||
# if len(key['content']) > 40:
|
db_model.list_ssh_public_keys_for_account(session["account"])
|
||||||
# print(key['content'])
|
))
|
||||||
# print(f"{key['content'][:20]}...{key['content'][len(key['content'])-20:]}")
|
|
||||||
# key["content"] =
|
|
||||||
|
|
||||||
# return
|
return render_template("ssh-public-keys.html", ssh_public_keys=keys_list, has_ssh_public_keys=len(keys_list) > 0)
|
||||||
|
|
||||||
return render_template(
|
|
||||||
"ssh-public-keys.html",
|
|
||||||
ssh_public_keys=map(
|
|
||||||
lambda x: dict(name=x['name'], content=f"{x['content'][:20]}...{x['content'][len(x['content'])-20:]}"),
|
|
||||||
db_model.list_ssh_public_keys_for_account(session["account"])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@bp.route("/create", methods=("GET", "POST"))
|
@bp.route("/create", methods=("GET", "POST"))
|
||||||
@account_required
|
@account_required
|
||||||
@ -89,22 +90,45 @@ def create():
|
|||||||
vm_sizes = db_model.vm_sizes_dict()
|
vm_sizes = db_model.vm_sizes_dict()
|
||||||
operating_systems = db_model.operating_systems_dict()
|
operating_systems = db_model.operating_systems_dict()
|
||||||
ssh_public_keys = db_model.list_ssh_public_keys_for_account(session["account"])
|
ssh_public_keys = db_model.list_ssh_public_keys_for_account(session["account"])
|
||||||
error = None
|
errors = list()
|
||||||
|
created_os = None
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
|
||||||
size = request.form["size"]
|
size = request.form["size"]
|
||||||
os = request.form["os"]
|
os = request.form["os"]
|
||||||
if not size:
|
if not size:
|
||||||
error = "Size is required"
|
errors.append("Size is required")
|
||||||
elif size not in vm_sizes:
|
elif size not in vm_sizes:
|
||||||
error = f"Invalid size {size}"
|
errors.append(f"Invalid size {size}")
|
||||||
|
|
||||||
if not os:
|
if not os:
|
||||||
error = "OS is required"
|
errors.append("OS is required")
|
||||||
elif os not in operating_systems:
|
elif os not in operating_systems:
|
||||||
error = f"Invalid os {os}"
|
errors.append(f"Invalid os {os}")
|
||||||
|
|
||||||
if error is None:
|
posted_keys_count = int(request.form["ssh_public_key_count"])
|
||||||
|
posted_keys_contents = list()
|
||||||
|
|
||||||
|
if posted_keys_count > 1000:
|
||||||
|
errors.append("something went wrong with ssh keys")
|
||||||
|
else:
|
||||||
|
for i in range(0, posted_keys_count):
|
||||||
|
if f"ssh_key_{i}" in request.form:
|
||||||
|
posted_name = request.form[f"ssh_key_{i}"]
|
||||||
|
key_content = None
|
||||||
|
for key in ssh_public_keys:
|
||||||
|
if key['name'] == posted_name:
|
||||||
|
key_content = key['content']
|
||||||
|
if key_content:
|
||||||
|
posted_keys_contents.append(key_content)
|
||||||
|
else:
|
||||||
|
errors.append(f"SSH Key \"{posted_name}\" doesn't exist")
|
||||||
|
|
||||||
|
if len(posted_keys_contents) == 0:
|
||||||
|
errors.append("At least one SSH Public Key is required")
|
||||||
|
|
||||||
|
if len(errors) == 0:
|
||||||
id = makeCapsulId()
|
id = makeCapsulId()
|
||||||
db_model.create_vm(
|
db_model.create_vm(
|
||||||
email=session["account"],
|
email=session["account"],
|
||||||
@ -115,17 +139,22 @@ def create():
|
|||||||
current_app.config["VIRTUALIZATION_MODEL"].create(
|
current_app.config["VIRTUALIZATION_MODEL"].create(
|
||||||
email = session["account"],
|
email = session["account"],
|
||||||
id=id,
|
id=id,
|
||||||
template_image_file_name=operating_systems[os].template_image_file_name,
|
template_image_file_name=operating_systems[os]['template_image_file_name'],
|
||||||
vcpus=vm_sizes[size].vcpus,
|
vcpus=vm_sizes[size]['vcpus'],
|
||||||
memory=vm_sizes[size].memory
|
memory_mb=vm_sizes[size]['memory_mb'],
|
||||||
|
ssh_public_keys=posted_keys_contents
|
||||||
)
|
)
|
||||||
|
created_os = os
|
||||||
if error:
|
|
||||||
|
for error in errors:
|
||||||
flash(error)
|
flash(error)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"create-capsul.html",
|
"create-capsul.html",
|
||||||
|
created_os=created_os,
|
||||||
ssh_public_keys=ssh_public_keys,
|
ssh_public_keys=ssh_public_keys,
|
||||||
|
ssh_public_key_count=len(ssh_public_keys),
|
||||||
|
has_ssh_public_keys=len(ssh_public_keys) > 0,
|
||||||
operating_systems=operating_systems,
|
operating_systems=operating_systems,
|
||||||
vm_sizes=vm_sizes
|
vm_sizes=vm_sizes
|
||||||
)
|
)
|
||||||
|
@ -34,7 +34,7 @@ class DBModel:
|
|||||||
|
|
||||||
def all_vm_ids(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 list(map(lambda x: x[0], self.cursor.fetchall()))
|
||||||
|
|
||||||
def operating_systems_dict(self,):
|
def operating_systems_dict(self,):
|
||||||
self.cursor.execute("SELECT id, template_image_file_name, description FROM os_images")
|
self.cursor.execute("SELECT id, template_image_file_name, description FROM os_images")
|
||||||
@ -56,10 +56,10 @@ class DBModel:
|
|||||||
|
|
||||||
def list_ssh_public_keys_for_account(self, email):
|
def list_ssh_public_keys_for_account(self, email):
|
||||||
self.cursor.execute("SELECT name, content, created FROM ssh_public_keys WHERE email = %s", (email, ))
|
self.cursor.execute("SELECT name, content, created FROM ssh_public_keys WHERE email = %s", (email, ))
|
||||||
return map(
|
return list(map(
|
||||||
lambda x: dict(name=x[0], content=x[1], created=x[2]),
|
lambda x: dict(name=x[0], content=x[1], created=x[2]),
|
||||||
self.cursor.fetchall()
|
self.cursor.fetchall()
|
||||||
)
|
))
|
||||||
|
|
||||||
def ssh_public_key_name_exists(self, email, name):
|
def ssh_public_key_name_exists(self, email, name):
|
||||||
self.cursor.execute( "SELECT name FROM ssh_public_keys where email = %s AND name = %s", (email, name) )
|
self.cursor.execute( "SELECT name FROM ssh_public_keys where email = %s AND name = %s", (email, name) )
|
||||||
@ -80,15 +80,14 @@ class DBModel:
|
|||||||
|
|
||||||
def list_vms_for_account(self, email):
|
def list_vms_for_account(self, email):
|
||||||
self.cursor.execute("""
|
self.cursor.execute("""
|
||||||
SELECT vms.id, vms.last_seen_ipv4, vms.last_seen_ipv6, vms.size, os_images.description, vms.created, vms.deleted
|
SELECT vms.id, vms.last_seen_ipv4, vms.last_seen_ipv6, vms.size, vms.os, vms.created, vms.deleted
|
||||||
FROM vms JOIN os_images on os_images.id = vms.os
|
FROM vms WHERE vms.email = %s""",
|
||||||
WHERE vms.email = %s""",
|
|
||||||
(email, )
|
(email, )
|
||||||
)
|
)
|
||||||
return map(
|
return list(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]),
|
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()
|
self.cursor.fetchall()
|
||||||
)
|
))
|
||||||
|
|
||||||
def create_vm(self, email, id, size, os):
|
def create_vm(self, email, id, size, os):
|
||||||
self.cursor.execute("""
|
self.cursor.execute("""
|
||||||
|
@ -30,10 +30,13 @@ a:hover, a:active, a:visited {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
.nav-row a {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.nav-row:last-child {
|
.nav-row:last-child {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.nav-row:last-child a, .nav-row:last-child div {
|
.nav-row:last-child a {
|
||||||
margin: 0 1em;
|
margin: 0 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,13 +76,13 @@ main {
|
|||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.row.wrap {
|
.wrap {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.row.justify-start {
|
.justify-start {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
.row.justify-end {
|
.justify-end {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,6 +119,7 @@ select {
|
|||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: bottom 0.65em right 0.8em;
|
background-position: bottom 0.65em right 0.8em;
|
||||||
background-size: 0.5em;
|
background-size: 0.5em;
|
||||||
|
padding-right: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
input, textarea {
|
input, textarea {
|
||||||
@ -139,14 +143,16 @@ textarea {
|
|||||||
height: 6em;
|
height: 6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type=checkbox] {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
input[type=submit], select, textarea {
|
input[type=submit], select, textarea {
|
||||||
font: calc(0.40rem + 1vmin) monospace;
|
font: calc(0.40rem + 1vmin) monospace;
|
||||||
border: 1px solid #777e73;
|
border: 1px solid #777e73;
|
||||||
background-color: #bdc7b810;
|
background-color: #bdc7b810;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
input[type=submit], select {
|
input[type=submit], select {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@ -162,6 +168,31 @@ ul li {
|
|||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table{
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
thead {
|
||||||
|
background: #bdc7b812;
|
||||||
|
}
|
||||||
|
td, th {
|
||||||
|
padding: 0.2em 1em;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
border-right: 4px solid #241e1e;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
border-bottom: 2px dotted #777e7355;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting-pulse {
|
||||||
|
animation: waiting-pulse 1s ease-in-out 0s infinite forwards alternate;
|
||||||
|
}
|
||||||
|
@keyframes waiting-pulse {
|
||||||
|
from { color: rgba(221, 169, 56, 0.8); }
|
||||||
|
to { color: rgba(221, 169, 56, 0.2); }
|
||||||
|
}
|
||||||
|
|
||||||
.code {
|
.code {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.5em 2em;
|
padding: 0.5em 2em;
|
||||||
|
@ -21,12 +21,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-row half-margin">
|
<div class="nav-row half-margin wrap">
|
||||||
<a href="/faq">FAQ</a>
|
<a href="/faq">FAQ</a>
|
||||||
<a href="/changelog">Changelog</a>
|
<a href="/changelog">Changelog</a>
|
||||||
|
|
||||||
{% if session["account"] %}
|
{% if session["account"] %}
|
||||||
<a href="/console/">Console</a>
|
<a href="/console">Capsuls</a>
|
||||||
|
<a href="/console/ssh">SSH Public Keys</a>
|
||||||
|
<a href="/console/billing">Account Balance</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<a href="/support">Support</a>
|
<a href="/support">Support</a>
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
{% extends 'console-base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %}Console{% endblock %}
|
{% block title %}Console{% endblock %}
|
||||||
{% block consoletitle %}Console{% endblock %}
|
|
||||||
|
|
||||||
{% block consolecontent %}
|
{% block content %}
|
||||||
{% if vms[0] is defined %}
|
<div class="third-margin">
|
||||||
|
<h1>Capsuls</h1>
|
||||||
|
</div>
|
||||||
|
<div class="third-margin">
|
||||||
|
{% if has_vms %}
|
||||||
|
<div class="row third-margin justify-end">
|
||||||
|
<a href="/console/create">Create Capsul</a>
|
||||||
|
</div>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -17,21 +23,20 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for vm in vms %}
|
{% for vm in vms %}
|
||||||
<a href="/console/{{ vm['id'] }}">
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ vm["id"] }}</td>
|
<td><a href="/console/{{ vm['id'] }}">{{ vm["id"] }}</a></td>
|
||||||
<td>{{ vm["size"] }}</td>
|
<td>{{ vm["size"] }}</td>
|
||||||
<td>{{ vm["ipv4"] }}</td>
|
<td class="{{ vm['ipv4_status'] }}">{{ vm["ipv4"] }}</td>
|
||||||
<td>{{ vm["os"] }}</td>
|
<td>{{ vm["os"] }}</td>
|
||||||
<td>{{ vm["created"] }}</td>
|
<td>{{ vm["created"] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</a>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
You don't have any Capsuls running. <a href="/console/create">Create one</a> today!
|
You don't have any Capsuls running. <a href="/console/create">Create one</a> today!
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block pagesource %}/templates/capsuls.html{% endblock %}
|
{% block pagesource %}/templates/capsuls.html{% endblock %}
|
||||||
|
@ -1,11 +1,26 @@
|
|||||||
{% extends 'console-base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %}Create{% endblock %}
|
{% block title %}Create{% endblock %}
|
||||||
{% block consoletitle %}Console - Create Capsul{% endblock %}
|
|
||||||
|
|
||||||
{% block consolecontent %}
|
{% block content %}
|
||||||
|
<div class="third-margin">
|
||||||
{% if ssh_public_keys[0] is defined %}
|
<h1>Create Capsul</h1>
|
||||||
|
</div>
|
||||||
|
<div class="third-margin">
|
||||||
|
{% if created_os %}
|
||||||
|
<p>
|
||||||
|
Your Capsul was successfully created! You should already see it listed on the
|
||||||
|
<a href="/console/">Capsuls page</a>, but it may not have obtained an IP address yet.
|
||||||
|
Its IP address should become visible once the machine has booted and taken a DHCP lease.
|
||||||
|
</p>
|
||||||
|
{% if created_os == 'debian10' %}
|
||||||
|
<p>
|
||||||
|
Note: because Debian delays fully booting until after entropy has been generated, Debian Capsuls
|
||||||
|
may take an extra-long time to obtain an IP address, like up to 10 minutes. Be patient.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% if has_ssh_public_keys %}
|
||||||
<pre>
|
<pre>
|
||||||
CAPSUL SIZES
|
CAPSUL SIZES
|
||||||
============
|
============
|
||||||
@ -36,6 +51,18 @@ f1-xxx $57.58 8 16G 25G 16TB
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row justify-start">
|
||||||
|
<input type="hidden" name="ssh_public_key_count" value="{{ ssh_public_key_count}}"/>
|
||||||
|
<label class="align" for="ssh_keys">SSH Public Keys</label>
|
||||||
|
<div id="ssh_keys">
|
||||||
|
{% for key in ssh_public_keys %}
|
||||||
|
<label for="ssh_key_{{ loop.index - 1 }}">
|
||||||
|
<input type="checkbox" id="ssh_key_{{ loop.index - 1 }}" name="ssh_key_{{ loop.index - 1 }}" value="{{ key['name'] }}"/>
|
||||||
|
{{ key['name'] }}
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row justify-end">
|
<div class="row justify-end">
|
||||||
<input type="submit" value="Create">
|
<input type="submit" value="Create">
|
||||||
</div>
|
</div>
|
||||||
@ -44,7 +71,9 @@ f1-xxx $57.58 8 16G 25G 16TB
|
|||||||
<p>You don't have any ssh public keys yet.</p>
|
<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>
|
<p>You must <a href="/console/ssh/upload">upload one</a> before you can create a Capsul.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block subcontent %}
|
{% block subcontent %}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
{% extends 'console-base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %}SSH Public Keys{% endblock %}
|
{% block title %}SSH Public Keys{% endblock %}
|
||||||
{% block consoletitle %}Console - SSH Public Keys{% endblock %}
|
|
||||||
|
|
||||||
{% block consolecontent %}
|
{% block content %}
|
||||||
|
<div class="third-margin">
|
||||||
|
<h1>SSH PUBLIC KEYS</h1>
|
||||||
{% if ssh_public_keys[0] is defined %} <hr/> {% endif %}
|
</div>
|
||||||
|
<div class="third-margin">
|
||||||
|
{% if has_ssh_public_keys %} <hr/> {% endif %}
|
||||||
|
|
||||||
{% for ssh_public_key in ssh_public_keys %}
|
{% for ssh_public_key in ssh_public_keys %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
@ -20,7 +21,7 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if ssh_public_keys[0] is defined %} <hr/> {% endif %}
|
{% if has_ssh_public_keys %} <hr/> {% endif %}
|
||||||
|
|
||||||
<div class="third-margin">
|
<div class="third-margin">
|
||||||
<h1>UPLOAD A NEW SSH KEY</h1>
|
<h1>UPLOAD A NEW SSH KEY</h1>
|
||||||
@ -50,6 +51,7 @@
|
|||||||
<input type="submit" value="Upload">
|
<input type="submit" value="Upload">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block pagesource %}/templates/ssh-public-keys.html{% endblock %}
|
{% block pagesource %}/templates/ssh-public-keys.html{% endblock %}
|
||||||
|
@ -25,7 +25,7 @@ class VirtualizationInterface:
|
|||||||
def list_ids(self) -> list:
|
def list_ids(self) -> list:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def create(self, email: str, id: str, template_image_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):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def destroy(self, email: str, id: str):
|
def destroy(self, email: str, id: str):
|
||||||
@ -42,8 +42,7 @@ class MockVirtualization(VirtualizationInterface):
|
|||||||
def create(self, email: str, id: str, template_image_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):
|
||||||
validate_capsul_id(id)
|
validate_capsul_id(id)
|
||||||
print(f"mock create: {id} for {email}")
|
print(f"mock create: {id} for {email}")
|
||||||
sleep(5)
|
sleep(1)
|
||||||
return VirtualMachine(id, ipv4="1.1.1.1")
|
|
||||||
|
|
||||||
def destroy(self, email: str, id: str):
|
def destroy(self, email: str, id: str):
|
||||||
print(f"mock destroy: {id} for {email}")
|
print(f"mock destroy: {id} for {email}")
|
||||||
@ -126,22 +125,6 @@ class ShellScriptVirtualization(VirtualizationInterface):
|
|||||||
stderr:
|
stderr:
|
||||||
{completedProcess.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 {id} ({email}) to obtain an IP address:
|
|
||||||
{vmSettings}
|
|
||||||
""")
|
|
||||||
|
|
||||||
def destroy(self, email: str, id: str):
|
def destroy(self, email: str, id: str):
|
||||||
validate_capsul_id(id)
|
validate_capsul_id(id)
|
||||||
|
Loading…
Reference in New Issue
Block a user