capsul-flask/capsulflask/console.py

278 lines
8.5 KiB
Python

import re
import sys
from datetime import datetime
from flask import Blueprint
from flask import flash
from flask import current_app
from flask import g
from flask import request
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.metrics import durations as metric_durations
from capsulflask.auth import account_required
from capsulflask.db import get_model, my_exec_info_message
bp = Blueprint("console", __name__, url_prefix="/console")
def makeCapsulId():
lettersAndNumbers = generate(alphabet="1234567890qwertyuiopasdfghjklzxcvbnm", size=10)
return f"capsul-{lettersAndNumbers}"
def double_check_capsul_address(id, ipv4):
try:
result = current_app.config["VIRTUALIZATION_MODEL"].get(id)
if result.ipv4 != ipv4:
ipv4 = result.ipv4
get_model().updateVm(email=session["account"], id=id, ipv4=result.ipv4)
except:
print(f"""
the virtualization model threw an error in double_check_capsul_address of {id}:
{my_exec_info_message(sys.exc_info())}"""
)
return ipv4
@bp.route("/")
@account_required
def index():
vms = get_vms()
# for now we are going to check the IP according to the virt model
# on every request. this could be done by a background job and cached later on...
for vm in vms:
vm["ipv4"] = double_check_capsul_address(vm["id"], vm["ipv4"])
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")
),
vms
))
return render_template("capsuls.html", vms=vms, has_vms=len(vms) > 0)
@bp.route("/<string:id>")
@account_required
def detail(id):
duration=request.args.get('duration')
if not duration:
duration = "5m"
vm = get_model().get_vm_detail(email=session["account"], id=id)
if vm is None:
return abort(404, f"{id} doesn't exist.")
vm["ipv4"] = double_check_capsul_address(vm["id"], vm["ipv4"])
vm["created"] = vm['created'].strftime("%b %d %Y %H:%M")
vm["ssh_public_keys"] = ", ".join(vm["ssh_public_keys"]) if len(vm["ssh_public_keys"]) > 0 else "<deleted>"
return render_template(
"capsul-detail.html",
vm=vm,
durations=list(map(lambda x: x.strip("_"), metric_durations.keys())),
duration=duration
)
@bp.route("/create", methods=("GET", "POST"))
@account_required
def create():
vm_sizes = get_model().vm_sizes_dict()
operating_systems = get_model().operating_systems_dict()
ssh_public_keys = get_model().list_ssh_public_keys_for_account(session["account"])
account_balance = get_account_balance()
errors = list()
created_os = None
if request.method == "POST":
size = request.form["size"]
os = request.form["os"]
if not size:
errors.append("Size is required")
elif size not in vm_sizes:
errors.append(f"Invalid size {size}")
if not os:
errors.append("OS is required")
elif os not in operating_systems:
errors.append(f"Invalid os {os}")
posted_keys_count = int(request.form["ssh_public_key_count"])
posted_keys = 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 = None
for x in ssh_public_keys:
if x['name'] == posted_name:
key = x
if key:
posted_keys.append(key)
else:
errors.append(f"SSH Key \"{posted_name}\" doesn't exist")
if len(posted_keys) == 0:
errors.append("At least one SSH Public Key is required")
if len(errors) == 0:
id = makeCapsulId()
get_model().create_vm(
email=session["account"],
id=id,
size=size,
os=os,
ssh_public_keys=list(map(lambda x: x["name"], posted_keys))
)
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_mb=vm_sizes[size]['memory_mb'],
ssh_public_keys=list(map(lambda x: x["content"], posted_keys))
)
created_os = os
affordable_vm_sizes = dict()
for key, vm_size in vm_sizes.items():
if vm_size["dollars_per_month"] < account_balance:
affordable_vm_sizes[key] = vm_size
for error in errors:
flash(error)
return render_template(
"create-capsul.html",
created_os=created_os,
account_balance=format(account_balance, '.2f'),
ssh_public_keys=ssh_public_keys,
ssh_public_key_count=len(ssh_public_keys),
no_ssh_public_keys=len(ssh_public_keys) == 0,
operating_systems=operating_systems,
cant_afford=len(affordable_vm_sizes) == 0,
vm_sizes=affordable_vm_sizes
)
@bp.route("/ssh", methods=("GET", "POST"))
@account_required
def ssh_public_keys():
errors = list()
if request.method == "POST":
method = request.form["method"]
content = None
name = request.form["name"]
if not name or len(name.strip()) < 1:
if method == "POST":
parts = re.split(" +", request.form["content"])
if len(parts) > 2 and len(parts[2].strip()) > 0:
name = parts[2]
else:
name = parts[0]
else:
errors.append("Name is required")
elif not re.match(r"^[0-9A-Za-z_@. -]+$", name):
errors.append("Name must match \"^[0-9A-Za-z_@. -]+$\"")
if method == "POST":
content = request.form["content"]
if not content or len(content.strip()) < 1:
errors.append("Content is required")
else:
content = content.replace("\r", "").replace("\n", "")
if not re.match(r"^(ssh|ecdsa)-[0-9A-Za-z+/_=@. -]+$", content):
errors.append("Content must match \"^(ssh|ecdsa)-[0-9A-Za-z+/_=@. -]+$\"")
if get_model().ssh_public_key_name_exists(session["account"], name):
errors.append("A key with that name already exists")
if len(errors) == 0:
get_model().create_ssh_public_key(session["account"], name, content)
elif method == "DELETE":
if len(errors) == 0:
get_model().delete_ssh_public_key(session["account"], name)
for error in errors:
flash(error)
keys_list=list(map(
lambda x: dict(name=x['name'], content=f"{x['content'][:20]}...{x['content'][len(x['content'])-20:]}"),
get_model().list_ssh_public_keys_for_account(session["account"])
))
return render_template("ssh-public-keys.html", ssh_public_keys=keys_list, has_ssh_public_keys=len(keys_list) > 0)
def get_vms():
if 'user_vms' not in g:
g.user_vms = get_model().list_vms_for_account(session["account"])
return g.user_vms
def get_payments():
if 'user_payments' not in g:
g.user_payments = get_model().list_payments_for_account(session["account"])
return g.user_payments
average_number_of_days_in_a_month = 30.44
def get_account_balance():
vm_cost_dollars = 0.0
for vm in get_vms():
end_datetime = vm["deleted"] if vm["deleted"] else datetime.utcnow()
vm_months = ( end_datetime - vm["created"] ).days / average_number_of_days_in_a_month
vm_cost_dollars += vm_months * float(vm["dollars_per_month"])
payment_dollars_total = float( sum(map(lambda x: x["dollars"], get_payments())) )
return payment_dollars_total - vm_cost_dollars
@bp.route("/account-balance")
@account_required
def account_balance():
payments = get_payments()
account_balance = get_account_balance()
vms_billed = list()
for vm in get_vms():
end_datetime = vm["deleted"] if vm["deleted"] else datetime.utcnow()
print(end_datetime)
print(vm["created"])
vm_months = (end_datetime - vm["created"]).days / average_number_of_days_in_a_month
vms_billed.append(dict(
id=vm["id"],
dollars_per_month=vm["dollars_per_month"],
created=vm["created"].strftime("%b %d %Y"),
deleted=vm["deleted"].strftime("%b %d %Y") if vm["deleted"] else "N/A",
months=format(vm_months, '.3f'),
dollars=format(vm_months * float(vm["dollars_per_month"]), '.2f')
))
return render_template(
"account-balance.html",
has_vms=len(vms_billed)>0,
vms_billed=vms_billed,
payments=list(map(lambda x: dict(dollars=x["dollars"], created=x["created"].strftime("%b %d %Y")), payments)),
has_payments=len(payments)>0,
account_balance=format(account_balance, '.2f')
)