13 Commits

Author SHA1 Message Date
3wc
aa1a9f558b Updates for upstream IP handling 2021-07-23 02:56:31 +02:00
3wc
e6a14f8626 Add basic "create" API..
.. using server-side API tokens
2021-07-23 02:56:31 +02:00
3wc
c4ba5ea197 Merge branch 'master' of ssh://git.autonomic.zone:2222/3wordchant/capsul-flask 2021-07-21 23:46:37 +02:00
3wc
33f4551cf4 Merge branch 'docs-reshuffle' 2021-07-21 23:44:42 +02:00
3wc
0fa7fb28b5 Split README up into separate files, plus:
* forest's ReadMe docs changes
* add Configuration-type-stuff that lives in the database
2021-07-21 23:43:44 +02:00
be6e72028c define BTCPAY_ENABLED based on URL and btcpay key, pass it explicitly 2021-07-21 23:43:44 +02:00
3wc
bf7487f4f0 Don't load /btcpay if BTCPAY_PRIVATE_KEY un-set 2021-07-21 23:43:44 +02:00
3wc
8b0ce0ba71 Hide the BTCPay link if BTCPAY_PRIVATE_KEY un-set 2021-07-21 23:43:44 +02:00
3wc
bca570882e Add load_config_vars context processor..
..to allow accessing config variables in the templates.

This removes the need for adding config variables manually to template
contexts.
2021-07-21 23:43:44 +02:00
f3ae9aae23 remove class="small" from pricing table cuz normal size fits fine 2021-07-21 23:43:44 +02:00
3wc
827ca4a50b Auto-generate the pricing table from the database 2021-07-21 23:43:44 +02:00
f999adaf71 Add VMs to the database even with HUB_MODE=mock (#6)
https://todo.cyberia.club/~cyberia/services/83

Co-authored-by: 3wc <3wc.cyberia@doesthisthing.work>
Co-authored-by: forest <forest.n.johnson@gmail.com>
Reviewed-on: 3wordchant/capsul-flask#6
Co-authored-by: 3wordchant <3wordchant@noreply.git.autonomic.zone>
Co-committed-by: 3wordchant <3wordchant@noreply.git.autonomic.zone>
2021-07-21 23:26:10 +02:00
3wc
8f2becb9ee Fix SyntaxWarning on is not ""
`capsulflask/__init__.py:143: SyntaxWarning: "is not" with a literal. Did you mean "!="?`
2021-07-21 21:51:50 +02:00
13 changed files with 278 additions and 105 deletions

View File

@ -140,7 +140,7 @@ else:
app.config['HTTP_CLIENT'] = MyHTTPClient(timeout_seconds=int(app.config['INTERNAL_HTTP_TIMEOUT_SECONDS'])) app.config['HTTP_CLIENT'] = MyHTTPClient(timeout_seconds=int(app.config['INTERNAL_HTTP_TIMEOUT_SECONDS']))
app.config['BTCPAY_ENABLED'] = False app.config['BTCPAY_ENABLED'] = False
if app.config['BTCPAY_URL'] is not "": if app.config['BTCPAY_URL'] != "":
try: try:
app.config['BTCPAY_CLIENT'] = btcpay.Client(api_uri=app.config['BTCPAY_URL'], pem=app.config['BTCPAY_PRIVATE_KEY']) app.config['BTCPAY_CLIENT'] = btcpay.Client(api_uri=app.config['BTCPAY_URL'], pem=app.config['BTCPAY_PRIVATE_KEY'])
app.config['BTCPAY_ENABLED'] = True app.config['BTCPAY_ENABLED'] = True
@ -155,7 +155,6 @@ is_running_server = ('flask run' in command_line) or ('gunicorn' in command_line
app.logger.info(f"is_running_server: {is_running_server}") app.logger.info(f"is_running_server: {is_running_server}")
if app.config['HUB_MODE_ENABLED']: if app.config['HUB_MODE_ENABLED']:
if app.config['HUB_MODEL'] == "capsul-flask": if app.config['HUB_MODEL'] == "capsul-flask":
app.config['HUB_MODEL'] = hub_model.CapsulFlaskHub() app.config['HUB_MODEL'] = hub_model.CapsulFlaskHub()
@ -177,7 +176,9 @@ if app.config['HUB_MODE_ENABLED']:
from capsulflask import db from capsulflask import db
db.init_app(app, is_running_server) db.init_app(app, is_running_server)
from capsulflask import auth, landing, console, payment, metrics, cli, hub_api, admin from capsulflask import (
auth, landing, console, payment, metrics, cli, hub_api, publicapi, admin
)
app.register_blueprint(landing.bp) app.register_blueprint(landing.bp)
app.register_blueprint(auth.bp) app.register_blueprint(auth.bp)
@ -187,13 +188,13 @@ if app.config['HUB_MODE_ENABLED']:
app.register_blueprint(cli.bp) app.register_blueprint(cli.bp)
app.register_blueprint(hub_api.bp) app.register_blueprint(hub_api.bp)
app.register_blueprint(admin.bp) app.register_blueprint(admin.bp)
app.register_blueprint(publicapi.bp)
app.add_url_rule("/", endpoint="index") app.add_url_rule("/", endpoint="index")
if app.config['SPOKE_MODE_ENABLED']: if app.config['SPOKE_MODE_ENABLED']:
if app.config['SPOKE_MODEL'] == "shell-scripts": if app.config['SPOKE_MODEL'] == "shell-scripts":
app.config['SPOKE_MODEL'] = spoke_model.ShellScriptSpoke() app.config['SPOKE_MODEL'] = spoke_model.ShellScriptSpoke()
else: else:

View File

@ -1,3 +1,4 @@
from base64 import b64decode
import functools import functools
import re import re
@ -24,6 +25,15 @@ def account_required(view):
@functools.wraps(view) @functools.wraps(view)
def wrapped_view(**kwargs): def wrapped_view(**kwargs):
api_token = request.headers.get('authorization', None)
if api_token is not None:
email = get_model().authenticate_token(b64decode(api_token).decode('utf-8'))
if email is not None:
session.clear()
session["account"] = email
session["csrf-token"] = generate()
if session.get("account") is None or session.get("csrf-token") is None : if session.get("account") is None or session.get("csrf-token") is None :
return redirect(url_for("auth.login")) return redirect(url_for("auth.login"))

View File

@ -1,7 +1,9 @@
from base64 import b64encode
from datetime import datetime, timedelta
import json
import re import re
import sys import sys
import json
from datetime import datetime, timedelta
from flask import Blueprint from flask import Blueprint
from flask import flash from flask import flash
from flask import current_app from flask import current_app
@ -98,7 +100,6 @@ def index():
@bp.route("/<string:id>", methods=("GET", "POST")) @bp.route("/<string:id>", methods=("GET", "POST"))
@account_required @account_required
def detail(id): def detail(id):
duration=request.args.get('duration') duration=request.args.get('duration')
if not duration: if not duration:
duration = "5m" duration = "5m"
@ -188,6 +189,67 @@ def detail(id):
duration=duration duration=duration
) )
def _create(vm_sizes, operating_systems, public_keys_for_account, server_data):
errors = list()
size = server_data.get("size")
os = server_data.get("os")
posted_keys_count = int(server_data.get("ssh_authorized_key_count"))
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 = 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 server_data:
posted_name = server_data.get(f"ssh_key_{i}")
key = None
for x in public_keys_for_account:
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")
capacity_avaliable = current_app.config["HUB_MODEL"].capacity_avaliable(
vm_sizes[size]['memory_mb']*1024*1024
)
if not capacity_avaliable:
errors.append("""
host(s) at capacity. no capsuls can be created at this time. sorry.
""")
if len(errors) == 0:
id = make_capsul_id()
current_app.config["HUB_MODEL"].create(
email = session["account"],
id=id,
os=os,
size=size,
template_image_file_name=operating_systems[os]['template_image_file_name'],
vcpus=vm_sizes[size]['vcpus'],
memory_mb=vm_sizes[size]['memory_mb'],
ssh_authorized_keys=list(map(lambda x: x["content"], posted_keys))
)
return id, errors
return None, errors
@bp.route("/create", methods=("GET", "POST")) @bp.route("/create", methods=("GET", "POST"))
@account_required @account_required
@ -202,62 +264,12 @@ def create():
if request.method == "POST": if request.method == "POST":
if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']: if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']:
return abort(418, f"u want tea") return abort(418, f"u want tea")
id, errors = _create(
size = request.form["size"] vm_sizes,
os = request.form["os"] operating_systems,
if not size: public_keys_for_account,
errors.append("Size is required") request.form)
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_authorized_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 public_keys_for_account:
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")
capacity_avaliable = current_app.config["HUB_MODEL"].capacity_avaliable(vm_sizes[size]['memory_mb']*1024*1024)
if not capacity_avaliable:
errors.append("""
host(s) at capacity. no capsuls can be created at this time. sorry.
""")
if len(errors) == 0: if len(errors) == 0:
id = make_capsul_id()
# we can't create the vm record in the DB yet because its IP address needs to be allocated first.
# so it will be created when the allocation happens inside the hub_api.
current_app.config["HUB_MODEL"].create(
email = session["account"],
id=id,
os=os,
size=size,
template_image_file_name=operating_systems[os]['template_image_file_name'],
vcpus=vm_sizes[size]['vcpus'],
memory_mb=vm_sizes[size]['memory_mb'],
ssh_authorized_keys=list(map(lambda x: dict(name=x['name'], content=x['content']), posted_keys))
)
return redirect(f"{url_for('console.index')}?created={id}") return redirect(f"{url_for('console.index')}?created={id}")
affordable_vm_sizes = dict() affordable_vm_sizes = dict()
@ -287,23 +299,25 @@ def create():
vm_sizes=affordable_vm_sizes vm_sizes=affordable_vm_sizes
) )
@bp.route("/ssh", methods=("GET", "POST")) @bp.route("/keys", methods=("GET", "POST"))
@account_required @account_required
def ssh_public_keys(): def ssh_api_keys():
errors = list() errors = list()
token = None
if request.method == "POST": if request.method == "POST":
if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']: if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']:
return abort(418, f"u want tea") return abort(418, f"u want tea")
method = request.form["method"] action = request.form["action"]
if action == 'upload_ssh_key':
content = None content = None
if method == "POST":
content = request.form["content"].replace("\r", " ").replace("\n", " ").strip() content = request.form["content"].replace("\r", " ").replace("\n", " ").strip()
name = request.form["name"] name = request.form["name"]
if not name or len(name.strip()) < 1: if not name or len(name.strip()) < 1:
if method == "POST":
parts = re.split(" +", content) parts = re.split(" +", content)
if len(parts) > 2 and len(parts[2].strip()) > 0: if len(parts) > 2 and len(parts[2].strip()) > 0:
name = parts[2].strip() name = parts[2].strip()
@ -314,7 +328,6 @@ def ssh_public_keys():
if not re.match(r"^[0-9A-Za-z_@:. -]+$", name): if not re.match(r"^[0-9A-Za-z_@:. -]+$", name):
errors.append(f"Key name '{name}' must match \"^[0-9A-Za-z_@:. -]+$\"") errors.append(f"Key name '{name}' must match \"^[0-9A-Za-z_@:. -]+$\"")
if method == "POST":
if not content or len(content.strip()) < 1: if not content or len(content.strip()) < 1:
errors.append("Content is required") errors.append("Content is required")
else: else:
@ -327,24 +340,36 @@ def ssh_public_keys():
if len(errors) == 0: if len(errors) == 0:
get_model().create_ssh_public_key(session["account"], name, content) get_model().create_ssh_public_key(session["account"], name, content)
elif method == "DELETE": elif action == "delete_ssh_key":
if len(errors) == 0:
get_model().delete_ssh_public_key(session["account"], name) get_model().delete_ssh_public_key(session["account"], name)
elif action == "generate_api_token":
name = request.form["name"]
if name == '':
name = datetime.utcnow().strftime('%y-%m-%d %H:%M:%S')
token = b64encode(
get_model().generate_api_token(session["account"], name).encode('utf-8')
).decode('utf-8')
elif action == "delete_api_token":
get_model().delete_api_token(session["account"], request.form["id"])
for error in errors: for error in errors:
flash(error) flash(error)
keys_list=list(map( ssh_keys_list=list(map(
lambda x: dict(name=x['name'], content=f"{x['content'][:20]}...{x['content'][len(x['content'])-20:]}"), 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"]) get_model().list_ssh_public_keys_for_account(session["account"])
)) ))
api_tokens_list = get_model().list_api_tokens(session["account"])
return render_template( return render_template(
"ssh-public-keys.html", "keys.html",
csrf_token = session["csrf-token"], csrf_token = session["csrf-token"],
ssh_public_keys=keys_list, api_tokens=api_tokens_list,
has_ssh_public_keys=len(keys_list) > 0 ssh_public_keys=ssh_keys_list,
generated_api_token=token,
) )
def get_vms(): def get_vms():
@ -368,7 +393,6 @@ def get_vm_months_float(vm, as_of):
return days / average_number_of_days_in_a_month return days / average_number_of_days_in_a_month
def get_account_balance(vms, payments, as_of): def get_account_balance(vms, payments, as_of):
vm_cost_dollars = 0.0 vm_cost_dollars = 0.0
for vm in vms: for vm in vms:
vm_months = get_vm_months_float(vm, as_of) vm_months = get_vm_months_float(vm, as_of)
@ -381,7 +405,6 @@ def get_account_balance(vms, payments, as_of):
@bp.route("/account-balance") @bp.route("/account-balance")
@account_required @account_required
def account_balance(): def account_balance():
payment_sessions = get_model().list_payment_sessions_for_account(session['account']) payment_sessions = get_model().list_payment_sessions_for_account(session['account'])
for payment_session in payment_sessions: for payment_session in payment_sessions:
if payment_session['type'] == 'btcpay': if payment_session['type'] == 'btcpay':

View File

@ -33,7 +33,7 @@ def init_app(app, is_running_server):
result = re.search(r"^\d+_(up|down)", filename) result = re.search(r"^\d+_(up|down)", filename)
if not result: if not result:
app.logger.error(f"schemaVersion {filename} must match ^\\d+_(up|down). exiting.") app.logger.error(f"schemaVersion {filename} must match ^\\d+_(up|down). exiting.")
exit(1) continue
key = result.group() key = result.group()
with open(join(schemaMigrationsPath, filename), 'rb') as file: with open(join(schemaMigrationsPath, filename), 'rb') as file:
schemaMigrations[key] = file.read().decode("utf8") schemaMigrations[key] = file.read().decode("utf8")
@ -43,7 +43,7 @@ def init_app(app, is_running_server):
hasSchemaVersionTable = False hasSchemaVersionTable = False
actionWasTaken = False actionWasTaken = False
schemaVersion = 0 schemaVersion = 0
desiredSchemaVersion = 18 desiredSchemaVersion = 19
cursor = connection.cursor() cursor = connection.cursor()
@ -128,4 +128,3 @@ def close_db(e=None):
if db_model is not None: if db_model is not None:
db_model.cursor.close() db_model.cursor.close()
current_app.config['PSYCOPG2_CONNECTION_POOL'].putconn(db_model.connection) current_app.config['PSYCOPG2_CONNECTION_POOL'].putconn(db_model.connection)

View File

@ -1,8 +1,8 @@
import re import re
# I was never able to get this type hinting to work correctly # I was never able to get this type hinting to work correctly
# from psycopg2.extensions import connection as Psycopg2Connection, cursor as Psycopg2Cursor # from psycopg2.extensions import connection as Psycopg2Connection, cursor as Psycopg2Cursor
import hashlib
from nanoid import generate from nanoid import generate
from flask import current_app from flask import current_app
from typing import List from typing import List
@ -17,7 +17,6 @@ class DBModel:
self.cursor = cursor self.cursor = cursor
# ------ LOGIN --------- # ------ LOGIN ---------
@ -44,6 +43,16 @@ class DBModel:
return (token, ignoreCaseMatches) return (token, ignoreCaseMatches)
def authenticate_token(self, token):
m = hashlib.md5()
m.update(token.encode('utf-8'))
hash_token = m.hexdigest()
self.cursor.execute("SELECT email FROM api_tokens WHERE token = %s", (hash_token, ))
result = self.cursor.fetchall()
if len(result) == 1:
return result[0]
return None
def consume_token(self, token): def consume_token(self, token):
self.cursor.execute("SELECT email FROM login_tokens WHERE token = %s and created > (NOW() - INTERVAL '20 min')", (token, )) self.cursor.execute("SELECT email FROM login_tokens WHERE token = %s and created > (NOW() - INTERVAL '20 min')", (token, ))
row = self.cursor.fetchone() row = self.cursor.fetchone()
@ -132,6 +141,32 @@ class DBModel:
self.cursor.execute( "DELETE FROM ssh_public_keys where email = %s AND name = %s", (email, name) ) self.cursor.execute( "DELETE FROM ssh_public_keys where email = %s AND name = %s", (email, name) )
self.connection.commit() self.connection.commit()
def list_api_tokens(self, email):
self.cursor.execute(
"SELECT id, token, name, created FROM api_tokens WHERE email = %s",
(email, )
)
return list(map(
lambda x: dict(id=x[0], token=x[1], name=x[2], created=x[3]),
self.cursor.fetchall()
))
def generate_api_token(self, email, name):
token = generate()
m = hashlib.md5()
m.update(token.encode('utf-8'))
hash_token = m.hexdigest()
self.cursor.execute(
"INSERT INTO api_tokens (email, name, token) VALUES (%s, %s, %s)",
(email, name, hash_token)
)
self.connection.commit()
return token
def delete_api_token(self, email, id_):
self.cursor.execute( "DELETE FROM api_tokens where email = %s AND id = %s", (email, id_))
self.connection.commit()
def list_vms_for_account(self, email): def list_vms_for_account(self, email):
self.cursor.execute(""" self.cursor.execute("""
SELECT vms.id, vms.public_ipv4, vms.public_ipv6, vms.size, vms.os, vms.created, vms.deleted, vm_sizes.dollars_per_month SELECT vms.id, vms.public_ipv4, vms.public_ipv6, vms.size, vms.os, vms.created, vms.deleted, vm_sizes.dollars_per_month
@ -479,8 +514,3 @@ class DBModel:
#cursor.close() #cursor.close()
return to_return return to_return

View File

@ -17,6 +17,10 @@ from capsulflask.http_client import HTTPResult
from capsulflask.shared import VirtualizationInterface, VirtualMachine, OnlineHost, validate_capsul_id, my_exec_info_message from capsulflask.shared import VirtualizationInterface, VirtualMachine, OnlineHost, validate_capsul_id, my_exec_info_message
class MockHub(VirtualizationInterface): class MockHub(VirtualizationInterface):
def __init__(self):
self.default_network = "public1"
self.default_ipv4 = "1.1.1.1"
def capacity_avaliable(self, additional_ram_bytes): def capacity_avaliable(self, additional_ram_bytes):
return True return True
@ -29,9 +33,9 @@ class MockHub(VirtualizationInterface):
{"key_type":"RSA", "content":"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvotgzgEP65JUQ8S8OoNKy1uEEPEAcFetSp7QpONe6hj4wPgyFNgVtdoWdNcU19dX3hpdse0G8OlaMUTnNVuRlbIZXuifXQ2jTtCFUA2mmJ5bF+XjGm3TXKMNGh9PN+wEPUeWd14vZL+QPUMev5LmA8cawPiU5+vVMLid93HRBj118aCJFQxLgrdP48VPfKHFRfCR6TIjg1ii3dH4acdJAvlmJ3GFB6ICT42EmBqskz2MPe0rIFxH8YohCBbAbrbWYcptHt4e48h4UdpZdYOhEdv89GrT8BF2C5cbQ5i9qVpI57bXKrj8hPZU5of48UHLSpXG8mbH0YDiOQOfKX/Mt", "sha256":"ghee6KzRnBJhND2kEUZSaouk7CD6o6z2aAc8GPkV+GQ"}, {"key_type":"RSA", "content":"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvotgzgEP65JUQ8S8OoNKy1uEEPEAcFetSp7QpONe6hj4wPgyFNgVtdoWdNcU19dX3hpdse0G8OlaMUTnNVuRlbIZXuifXQ2jTtCFUA2mmJ5bF+XjGm3TXKMNGh9PN+wEPUeWd14vZL+QPUMev5LmA8cawPiU5+vVMLid93HRBj118aCJFQxLgrdP48VPfKHFRfCR6TIjg1ii3dH4acdJAvlmJ3GFB6ICT42EmBqskz2MPe0rIFxH8YohCBbAbrbWYcptHt4e48h4UdpZdYOhEdv89GrT8BF2C5cbQ5i9qVpI57bXKrj8hPZU5of48UHLSpXG8mbH0YDiOQOfKX/Mt", "sha256":"ghee6KzRnBJhND2kEUZSaouk7CD6o6z2aAc8GPkV+GQ"},
{"key_type":"ECDSA", "content":"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLLgOoATz9R4aS2kk7vWoxX+lshK63t9+5BIHdzZeFE1o+shlcf0Wji8cN/L1+m3bi0uSETZDOAWMP3rHLJj9Hk=", "sha256":"aCYG1aD8cv/TjzJL0bi9jdabMGksdkfa7R8dCGm1yYs"} {"key_type":"ECDSA", "content":"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLLgOoATz9R4aS2kk7vWoxX+lshK63t9+5BIHdzZeFE1o+shlcf0Wji8cN/L1+m3bi0uSETZDOAWMP3rHLJj9Hk=", "sha256":"aCYG1aD8cv/TjzJL0bi9jdabMGksdkfa7R8dCGm1yYs"}
]""") ]""")
return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4="1.1.1.1", ssh_host_keys=ssh_host_keys) return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4=self.default_ipv4, ssh_host_keys=ssh_host_keys)
return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4="1.1.1.1") return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4=self.default_ipv4)
def list_ids(self) -> list: def list_ids(self) -> list:
return get_model().all_non_deleted_vm_ids() return get_model().all_non_deleted_vm_ids()
@ -40,6 +44,16 @@ class MockHub(VirtualizationInterface):
validate_capsul_id(id) validate_capsul_id(id)
current_app.logger.info(f"mock create: {id} for {email}") current_app.logger.info(f"mock create: {id} for {email}")
sleep(1) sleep(1)
get_model().create_vm(
email=email,
id=id,
size=size,
os=os,
host=current_app.config["SPOKE_HOST_ID"],
network_name=self.default_network,
public_ipv4=self.default_ipv4,
ssh_authorized_keys=list(map(lambda x: x["name"], ssh_authorized_keys)),
)
def destroy(self, email: str, id: str): def destroy(self, email: str, id: str):
current_app.logger.info(f"mock destroy: {id} for {email}") current_app.logger.info(f"mock destroy: {id} for {email}")
@ -49,7 +63,6 @@ class MockHub(VirtualizationInterface):
class CapsulFlaskHub(VirtualizationInterface): class CapsulFlaskHub(VirtualizationInterface):
def synchronous_operation(self, hosts: List[OnlineHost], email: str, payload: str) -> List[HTTPResult]: def synchronous_operation(self, hosts: List[OnlineHost], email: str, payload: str) -> List[HTTPResult]:
return self.generic_operation(hosts, email, payload, True)[1] return self.generic_operation(hosts, email, payload, True)[1]
@ -262,4 +275,3 @@ class CapsulFlaskHub(VirtualizationInterface):
if not result_status == "success": if not result_status == "success":
raise ValueError(f"""failed to {command} vm "{id}" on host "{host.id}" for {email}: {result_json_string}""") raise ValueError(f"""failed to {command} vm "{id}" on host "{host.id}" for {email}: {result_json_string}""")

40
capsulflask/publicapi.py Normal file
View File

@ -0,0 +1,40 @@
import datetime
from flask import Blueprint
from flask import current_app
from flask import jsonify
from flask import request
from flask import session
from nanoid import generate
from capsulflask.auth import account_required
from capsulflask.db import get_model
bp = Blueprint("publicapi", __name__, url_prefix="/api")
@bp.route("/capsul/create", methods=["POST"])
@account_required
def capsul_create():
email = session["account"]
from .console import _create,get_account_balance, get_payments, get_vms
vm_sizes = get_model().vm_sizes_dict()
operating_systems = get_model().operating_systems_dict()
public_keys_for_account = get_model().list_ssh_public_keys_for_account(session["account"])
account_balance = get_account_balance(get_vms(), get_payments(), datetime.datetime.utcnow())
capacity_avaliable = current_app.config["HUB_MODEL"].capacity_avaliable(512*1024*1024)
request.json['ssh_authorized_key_count'] = 1
id, errors = _create(
vm_sizes,
operating_systems,
public_keys_for_account,
request.json)
if id is not None:
return jsonify(
id=id,
)
return jsonify(errors=errors)

View File

@ -0,0 +1,2 @@
DROP TABLE api_keys;
UPDATE schemaversion SET version = 18;

View File

@ -0,0 +1,9 @@
CREATE TABLE api_tokens (
id SERIAL PRIMARY KEY,
email TEXT REFERENCES accounts(email) ON DELETE RESTRICT,
name TEXT NOT NULL,
created TIMESTAMP NOT NULL DEFAULT NOW(),
token TEXT NOT NULL
);
UPDATE schemaversion SET version = 19;

View File

@ -31,7 +31,7 @@
{% if session["account"] %} {% if session["account"] %}
<a href="/console">Capsuls</a> <a href="/console">Capsuls</a>
<a href="/console/ssh">SSH Public Keys</a> <a href="/console/keys">SSH &amp; API Keys</a>
<a href="/console/account-balance">Account Balance</a> <a href="/console/account-balance">Account Balance</a>
{% endif %} {% endif %}

View File

@ -101,7 +101,7 @@
</div> </div>
<div class="row justify-start"> <div class="row justify-start">
<label class="align" for="ssh_authorized_keys">SSH Authorized Keys</label> <label class="align" for="ssh_authorized_keys">SSH Authorized Keys</label>
<a id="ssh_authorized_keys" href="/console/ssh">{{ vm['ssh_authorized_keys'] }}</a> <a id="ssh_authorized_keys" href="/console/keys">{{ vm['ssh_authorized_keys'] }}</a>
</div> </div>
</div> </div>

View File

@ -31,7 +31,7 @@
<p>(At least one month of funding is required)</p> <p>(At least one month of funding is required)</p>
{% elif no_ssh_public_keys %} {% elif no_ssh_public_keys %}
<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 one</a> before you can create a Capsul.</p> <p>You must <a href="/console/keys">upload one</a> before you can create a Capsul.</p>
{% elif not capacity_avaliable %} {% elif not capacity_avaliable %}
<p>Host(s) at capacity. No capsuls can be created at this time. sorry. </p> <p>Host(s) at capacity. No capsuls can be created at this time. sorry. </p>
{% else %} {% else %}

View File

@ -1,17 +1,18 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}SSH Public Keys{% endblock %} {% block title %}SSH &amp; API Keys{% endblock %}
{% block content %} {% block content %}
<div class="row third-margin"> <div class="row third-margin">
<h1>SSH PUBLIC KEYS</h1> <h1>SSH PUBLIC KEYS</h1>
</div> </div>
<div class="row third-margin"><div> <div class="row third-margin"><div>
{% if has_ssh_public_keys %} <hr/> {% endif %} {% if ssh_public_keys|length > 0 %} <hr/> {% endif %}
{% for ssh_public_key in ssh_public_keys %} {% for ssh_public_key in ssh_public_keys %}
<form method="post"> <form method="post">
<input type="hidden" name="method" value="DELETE"></input> <input type="hidden" name="method" value="DELETE"></input>
<input type="hidden" name="action" value="delete_ssh_key"></input>
<input type="hidden" name="name" value="{{ ssh_public_key['name'] }}"></input> <input type="hidden" name="name" value="{{ ssh_public_key['name'] }}"></input>
<input type="hidden" name="csrf-token" value="{{ csrf_token }}"/> <input type="hidden" name="csrf-token" value="{{ csrf_token }}"/>
<div class="row"> <div class="row">
@ -22,13 +23,14 @@
</form> </form>
{% endfor %} {% endfor %}
{% if has_ssh_public_keys %} <hr/> {% endif %} {% if ssh_public_keys|length > 0 %} <hr/> {% endif %}
<div class="third-margin"> <div class="third-margin">
<h1>UPLOAD A NEW SSH PUBLIC KEY</h1> <h1>UPLOAD A NEW SSH PUBLIC KEY</h1>
</div> </div>
<form method="post"> <form method="post">
<input type="hidden" name="method" value="POST"></input> <input type="hidden" name="method" value="POST"></input>
<input type="hidden" name="action" value="upload_ssh_key"></input>
<input type="hidden" name="csrf-token" value="{{ csrf_token }}"/> <input type="hidden" name="csrf-token" value="{{ csrf_token }}"/>
<div class="row justify-start"> <div class="row justify-start">
<label class="align" for="content">File Contents</label> <label class="align" for="content">File Contents</label>
@ -54,6 +56,51 @@
</div> </div>
</form> </form>
</div></div> </div></div>
<hr/>
<div class="row third-margin">
<h1>API KEYS</h1>
</div>
<div class="row third-margin"><div>
{% if generated_api_token %}
<hr/>
Generated key:
<span class="code">{{ generated_api_token }}</span>
{% endif %}
{% if api_tokens|length >0 %} <hr/>{% endif %}
{% for api_token in api_tokens %}
<form method="post">
<input type="hidden" name="method" value="DELETE"></input>
<input type="hidden" name="action" value="delete_api_token"></input>
<input type="hidden" name="id" value="{{ api_token['id'] }}"></input>
<input type="hidden" name="csrf-token" value="{{ csrf_token }}"/>
<div class="row">
<span class="code">{{ api_token['name'] }}</span>
created {{ api_token['created'].strftime("%b %d %Y") }}
<input type="submit" value="Delete">
</div>
</form>
{% endfor %}
{% if api_tokens|length >0 %} <hr/>{% endif %}
<div class="third-margin">
<h1>GENERATE A NEW API KEY</h1>
</div>
<form method="post">
<input type="hidden" name="method" value="POST"></input>
<input type="hidden" name="action" value="generate_api_token"></input>
<input type="hidden" name="csrf-token" value="{{ csrf_token }}"/>
<div class="smalltext">
<p>Generate a new API key, to integrate with other systems.</p>
</div>
<div class="row justify-start">
<label class="align" for="name">Key Name</label>
<input type="text" id="name" name="name"></input> (defaults to creation time)
</div>
<div class="row justify-end">
<input type="submit" value="Generate">
</div>
</form>
</div></div>
{% endblock %} {% endblock %}
{% block pagesource %}/templates/ssh-public-keys.html{% endblock %} {% block pagesource %}/templates/ssh-public-keys.html{% endblock %}