first crack at adding ssh host key display to the capsul detail page

This commit is contained in:
forest 2021-01-30 01:39:48 -06:00
parent 29008bc963
commit 50cea6e0b4
13 changed files with 199 additions and 51 deletions

View File

@ -11,7 +11,8 @@ from flask import current_app
from psycopg2 import ProgrammingError
from flask_mail import Message
from capsulflask.db import get_model, my_exec_info_message
from capsulflask.db import get_model
from capsulflask.shared import my_exec_info_message
from capsulflask.console import get_account_balance
bp = Blueprint('cli', __name__)

View File

@ -1,5 +1,6 @@
import re
import sys
import json
from datetime import datetime, timedelta
from flask import Blueprint
from flask import flash
@ -15,7 +16,8 @@ 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
from capsulflask.db import get_model
from capsulflask.shared import my_exec_info_message
from capsulflask.payment import poll_btcpay_session
from capsulflask import cli
@ -25,19 +27,21 @@ def makeCapsulId():
lettersAndNumbers = generate(alphabet="1234567890qwertyuiopasdfghjklzxcvbnm", size=10)
return f"capsul-{lettersAndNumbers}"
def double_check_capsul_address(id, ipv4):
def double_check_capsul_address(id, ipv4, get_ssh_host_keys):
try:
result = current_app.config["VIRTUALIZATION_MODEL"].get(id)
result = current_app.config["VIRTUALIZATION_MODEL"].get(id, get_ssh_host_keys)
if result.ipv4 != ipv4:
ipv4 = result.ipv4
get_model().update_vm_ip(email=session["account"], id=id, ipv4=result.ipv4)
if get_ssh_host_keys:
get_model().update_vm_ssh_host_keys(email=session["account"], id=id, ssh_host_keys=result.ssh_host_keys)
except:
current_app.logger.error(f"""
the virtualization model threw an error in double_check_capsul_address of {id}:
{my_exec_info_message(sys.exc_info())}"""
)
return ipv4
return result
@bp.route("/")
@account_required
@ -53,7 +57,7 @@ def index():
# 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"])
vm["ipv4"] = double_check_capsul_address(vm["id"], vm["ipv4"], False).ipv4
vms = list(map(
lambda x: dict(
@ -104,9 +108,17 @@ def detail(id):
return render_template("capsul-detail.html", vm=vm, delete=True, deleted=True)
else:
vm["ipv4"] = double_check_capsul_address(vm["id"], vm["ipv4"])
needs_ssh_host_keys = "ssh_host_keys" not in vm or len(vm["ssh_host_keys"]) == 0
vm_from_virt_model = double_check_capsul_address(vm["id"], vm["ipv4"], needs_ssh_host_keys)
vm["ipv4"] = vm_from_virt_model.ipv4
if needs_ssh_host_keys:
vm["ssh_host_keys"] = vm_from_virt_model.ssh_host_keys
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 "<missing>"
vm["ssh_authorized_keys"] = ", ".join(vm["ssh_authorized_keys"]) if len(vm["ssh_authorized_keys"]) > 0 else "<missing>"
current_app.logger.info(f"asd {needs_ssh_host_keys} {json.dumps(vm['ssh_host_keys'])})")
return render_template(
"capsul-detail.html",
@ -123,12 +135,12 @@ def detail(id):
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"])
public_keys_for_account = get_model().list_ssh_public_keys_for_account(session["account"])
account_balance = get_account_balance(get_vms(), get_payments(), datetime.utcnow())
capacity_avaliable = current_app.config["VIRTUALIZATION_MODEL"].capacity_avaliable(512*1024*1024)
errors = list()
ssh_keys_from_db_string = "\n".join(list(map(lambda x: f"name: {x['name']}**content: {x['content']}", ssh_public_keys)))
ssh_keys_from_db_string = "\n".join(list(map(lambda x: f"name: {x['name']}**content: {x['content']}", public_keys_for_account)))
email_to_log = session["account"]
current_app.logger.info(f"create for {email_to_log}: ssh keys from db:\n {ssh_keys_from_db_string}")
@ -161,7 +173,7 @@ def create():
posted_name = request.form[f"ssh_key_{i}"]
current_app.logger.info(f"ssh key posted_name: {posted_name}")
key = None
for x in ssh_public_keys:
for x in public_keys_for_account:
if x['name'] == posted_name:
current_app.logger.info(f"ssh key posted_name {posted_name} was found")
key = x
@ -190,7 +202,7 @@ def create():
id=id,
size=size,
os=os,
ssh_public_keys=list(map(lambda x: x["name"], posted_keys))
ssh_authorized_keys=list(map(lambda x: x["name"], posted_keys))
)
current_app.config["VIRTUALIZATION_MODEL"].create(
email = session["account"],
@ -198,7 +210,7 @@ def create():
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))
ssh_authorized_keys=list(map(lambda x: x["content"], posted_keys))
)
return redirect(f"{url_for('console.index')}?created={id}")
@ -219,9 +231,9 @@ def create():
csrf_token = session["csrf-token"],
capacity_avaliable=capacity_avaliable,
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,
ssh_public_keys=public_keys_for_account,
ssh_public_key_count=len(public_keys_for_account),
no_ssh_public_keys=len(public_keys_for_account) == 0,
operating_systems=operating_systems,
cant_afford=len(affordable_vm_sizes) == 0,
vm_sizes=affordable_vm_sizes

View File

@ -9,6 +9,7 @@ from flask import current_app
from flask import g
from capsulflask.db_model import DBModel
from capsulflask.shared import my_exec_info_message
def init_app(app):
databaseUrl = urlparse(app.config['DATABASE_URL'])
@ -41,7 +42,7 @@ def init_app(app):
hasSchemaVersionTable = False
actionWasTaken = False
schemaVersion = 0
desiredSchemaVersion = 11
desiredSchemaVersion = 12
cursor = connection.cursor()
@ -127,5 +128,3 @@ def close_db(e=None):
db_model.cursor.close()
current_app.config['PSYCOPG2_CONNECTION_POOL'].putconn(db_model.connection)
def my_exec_info_message(exec_info):
return "{}: {}".format(".".join([exec_info[0].__module__, exec_info[0].__name__]), exec_info[1])

View File

@ -104,7 +104,17 @@ class DBModel:
self.cursor.execute("UPDATE vms SET last_seen_ipv4 = %s WHERE email = %s AND id = %s", (ipv4, email, id))
self.connection.commit()
def create_vm(self, email, id, size, os, ssh_public_keys):
def update_vm_ssh_host_keys(self, email, id, ssh_host_keys):
for key in ssh_host_keys:
self.cursor.execute("""
INSERT INTO vm_ssh_host_key (email, vm_id, key_type, content, sha256)
VALUES (%s, %s, %s, %s, %s)
""",
(email, id, key['key_type'], key['content'], key['sha256'])
)
self.connection.commit()
def create_vm(self, email, id, size, os, ssh_authorized_keys):
self.cursor.execute("""
INSERT INTO vms (email, id, size, os)
VALUES (%s, %s, %s, %s)
@ -112,14 +122,14 @@ class DBModel:
(email, id, size, os)
)
for ssh_public_key in ssh_public_keys:
current_app.logger.info(f"INSERT INTO vm_ssh_public_key (email, vm_id, ssh_public_key_name) VALUES (\"{email}\", \"{id}\", \"{ssh_public_key}\")")
for ssh_authorized_key in ssh_authorized_keys:
current_app.logger.info(f"INSERT INTO vm_ssh_authorized_key (email, vm_id, ssh_public_key_name) VALUES (\"{email}\", \"{id}\", \"{ssh_authorized_key}\")")
self.cursor.execute("""
INSERT INTO vm_ssh_public_key (email, vm_id, ssh_public_key_name)
INSERT INTO vm_ssh_authorized_key (email, vm_id, ssh_public_key_name)
VALUES (%s, %s, %s)
""",
(email, id, ssh_public_key)
(email, id, ssh_authorized_key)
)
self.connection.commit()
@ -147,11 +157,19 @@ class DBModel:
)
self.cursor.execute("""
SELECT ssh_public_key_name FROM vm_ssh_public_key
WHERE vm_ssh_public_key.email = %s AND vm_ssh_public_key.vm_id = %s""",
SELECT ssh_public_key_name FROM vm_ssh_authorized_key
WHERE vm_ssh_authorized_key.email = %s AND vm_ssh_authorized_key.vm_id = %s""",
(email, id)
)
vm["ssh_public_keys"] = list(map( lambda x: x[0], self.cursor.fetchall() ))
vm["ssh_authorized_keys"] = list(map( lambda x: x[0], self.cursor.fetchall() ))
self.cursor.execute("""
SELECT key_type, content, sha256 FROM vm_ssh_host_key
WHERE vm_ssh_host_key.email = %s AND vm_ssh_host_key.vm_id = %s""",
(email, id)
)
vm["ssh_host_keys"] = list(map( lambda x: dict(key_type=x[0], content=x[1], sha256=x[2]), self.cursor.fetchall() ))
return vm

View File

@ -5,6 +5,7 @@ from functools import reduce
import requests
#import json
from datetime import datetime
from threading import Lock
from io import BytesIO
from flask import Blueprint
from flask import current_app
@ -15,6 +16,7 @@ from werkzeug.exceptions import abort
from capsulflask.db import get_model
from capsulflask.auth import account_required
mutex = Lock()
bp = Blueprint("metrics", __name__, url_prefix="/metrics")
durations = dict(
@ -116,6 +118,7 @@ def get_plot_bytes(metric, capsulid, duration, size):
return (502, None)
series = prometheus_response.json()["data"]["result"]
if len(series) == 0:
now_timestamp = datetime.timestamp(datetime.now())
series = [
@ -129,13 +132,19 @@ def get_plot_bytes(metric, capsulid, duration, size):
series[0]["values"]
))
mutex.acquire()
try:
plot_bytes = draw_plot_png_bytes(time_series_data, scale=scales[metric], size_x=sizes[size][0], size_y=sizes[size][1])
finally:
mutex.release()
return (200, plot_bytes)
def draw_plot_png_bytes(data, scale, size_x=3, size_y=1):
#current_app.logger.info(json.dumps(data, indent=4, default=str))
pyplot.style.use("seaborn-dark")
fig, my_plot = pyplot.subplots(figsize=(size_x, size_y))

View File

@ -20,7 +20,8 @@ from werkzeug.exceptions import abort
from capsulflask.auth import account_required
from capsulflask.db import get_model, my_exec_info_message
from capsulflask.db import get_model
from capsulflask.shared import my_exec_info_message
bp = Blueprint("payment", __name__, url_prefix="/payment")

View File

@ -0,0 +1,7 @@
DROP TABLE vm_ssh_host_key;
ALTER TABLE vm_ssh_authorized_key RENAME TO vm_ssh_public_key;
UPDATE schemaversion SET version = 11;

View File

@ -0,0 +1,14 @@
CREATE TABLE vm_ssh_host_key (
email TEXT NOT NULL,
vm_id TEXT NOT NULL,
key_type TEXT NOT NULL,
content TEXT NOT NULL,
sha256 TEXT NOT NULL,
FOREIGN KEY (email, vm_id) REFERENCES vms(email, id) ON DELETE CASCADE,
PRIMARY KEY (email, vm_id, key_type)
);
ALTER TABLE vm_ssh_public_key RENAME TO vm_ssh_authorized_key;
UPDATE schemaversion SET version = 12;

20
capsulflask/shared.py Normal file
View File

@ -0,0 +1,20 @@
from typing import List
# I decided to just use dict everywhere instead because I have to use dict to read it from json
# class SSHHostKey:
# def __init__(self, key_type=None, content=None, sha256=None):
# self.key_type = key_type
# self.content = content
# self.sha256 = sha256
class VirtualMachine:
def __init__(self, id, ipv4: str = None, ipv6: str = None, ssh_host_keys: List[dict] = list()):
self.id = id
self.ipv4 = ipv4
self.ipv6 = ipv6
self.ssh_host_keys = ssh_host_keys
def my_exec_info_message(exec_info):
return "{}: {}".format(".".join([exec_info[0].__module__, exec_info[0].__name__]), exec_info[1])

View File

@ -0,0 +1,22 @@
#!/bin/sh
ip_address="$1"
if echo "$ip_address" | grep -vqE '^([0-9]{1,3}\.){3}[0-9]{1,3}$'; then
echo "ip_address $ip_address must match "'"^([0-9]{1,3}\.){3}[0-9]{1,3}$"'
exit 1
fi
printf '['
DELIMITER="\n"
ssh-keyscan "$ip_address" 2>/dev/null | while read -r line; do
if echo "$line" | grep -qE "^$ip_address"' +(ssh|ecdsa)-[0-9A-Za-z+/_=@. -]+$'; then
KEY_CONTENT="$(echo "$line" | awk '{ print $2 " " $3 }')"
FINGERPRINT_OUTPUT="$(echo "$KEY_CONTENT" | ssh-keygen -l -E sha256 -f - | sed -E 's/^[0-9]+ SHA256:([0-9A-Za-z+/-]+) .+ \(([A-Z0-9]+)\)$/\1 \2/g')"
SHA256_HASH="$(echo "$FINGERPRINT_OUTPUT" | awk '{ print $1 }')"
KEY_TYPE="$(echo "$FINGERPRINT_OUTPUT" | awk '{ print $2 }')"
printf '%s{"key_type":"%s", "content":"%s", "sha256":"%s"}' "$DELIMITER" "$KEY_TYPE" "$KEY_CONTENT" "$SHA256_HASH"
DELIMITER=",\n"
fi
done
printf '\n]\n'

View File

@ -102,6 +102,9 @@ main {
.row.grid-large > div {
flex: 1 1 20em;
}
.row.grid-medium > div {
flex: 1 1 13em;
}
.row.grid-small > div {
flex: 0 0 8em;
}
@ -277,8 +280,12 @@ div.metric {
border: 1px solid #777e73;
background: #bdc7b810;
}
.break-word {
word-break: break-word;
pre.code.wrap {
white-space: pre-wrap;
}
.break-all {
word-break: break-all;
}
.dim {

View File

@ -73,8 +73,8 @@
<span id="ssh_username">cyberian</span>
</div>
<div class="row justify-start">
<label class="align" for="ssh_public_keys">SSH Public Keys</label>
<a id="ssh_public_keys" href="/console/ssh">{{ vm['ssh_public_keys'] }}</a>
<label class="align" for="ssh_authorized_keys">SSH Authorized Keys</label>
<a id="ssh_authorized_keys" href="/console/ssh">{{ vm['ssh_authorized_keys'] }}</a>
</div>
<div class="row center justify-start">
<label class="align" for="delete_action">Actions</label>
@ -85,6 +85,15 @@
</form>
</div>
</div>
<div class="row third-margin">
<h1>ssh host key fingerprints</h1>
</div>
<div class="row">
<pre class="code">{% for key in vm['ssh_host_keys'] %}
SHA256:{{ key.sha256 }} ({{ key.key_type }}){% endfor %}</pre>
</div>
<div class="row ">
<hr/>
</div>
@ -136,6 +145,17 @@
</a>
</div>
</div>
<div class="row ">
<hr/>
</div>
<div class="row half-margin">
add the following to your ~/.ssh/known_hosts file (optional)
</div>
<div class="row">
<pre class="code wrap break-all smalltext">{% for key in vm['ssh_host_keys'] %}
{{ vm['ipv4'] }} {{ key.content }}{% endfor %}
</pre>
</div>
{% endif %}
{% endblock %}

View File

@ -1,5 +1,7 @@
import subprocess
import re
import sys
import json
from flask import current_app
from time import sleep
@ -7,28 +9,23 @@ from os.path import join
from subprocess import run
from capsulflask.db import get_model
from capsulflask.shared import my_exec_info_message, VirtualMachine
def validate_capsul_id(id):
if not re.match(r"^(cvm|capsul)-[a-z0-9]{10}$", id):
raise ValueError(f"vm id \"{id}\" must match \"^capsul-[a-z0-9]{{10}}$\"")
class VirtualMachine:
def __init__(self, id, ipv4=None, ipv6=None):
self.id = id
self.ipv4 = ipv4
self.ipv6 = ipv6
class VirtualizationInterface:
def capacity_avaliable(self, additional_ram_bytes: int) -> bool:
pass
def get(self, id: str) -> VirtualMachine:
def get(self, id: str, get_ssh_host_keys: bool) -> VirtualMachine:
pass
def list_ids(self) -> list:
pass
def create(self, email: str, id: str, template_image_file_name: str, vcpus: int, memory: int, ssh_public_keys: list):
def create(self, email: str, id: str, template_image_file_name: str, vcpus: int, memory: int, ssh_authorized_keys: list):
pass
def destroy(self, email: str, id: str):
@ -38,14 +35,23 @@ class MockVirtualization(VirtualizationInterface):
def capacity_avaliable(self, additional_ram_bytes):
return True
def get(self, id):
def get(self, id, get_ssh_host_keys):
validate_capsul_id(id)
if get_ssh_host_keys:
ssh_host_keys = json.loads("""[
{"key_type":"ED25519", "content":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN8cna0zeKSKl/r8whdn/KmDWhdzuWRVV0GaKIM+eshh", "sha256":"V4X2apAF6btGAfS45gmpldknoDX0ipJ5c6DLfZR2ttQ"},
{"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"}
]""")
return VirtualMachine(id, ipv4="1.1.1.1", ssh_host_keys=ssh_host_keys)
return VirtualMachine(id, ipv4="1.1.1.1")
def list_ids(self) -> list:
return get_model().all_non_deleted_vm_ids()
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_authorized_keys: list):
validate_capsul_id(id)
current_app.logger.info(f"mock create: {id} for {email}")
sleep(1)
@ -91,19 +97,31 @@ class ShellScriptVirtualization(VirtualizationInterface):
return True
def get(self, id):
def get(self, id, get_ssh_host_keys):
validate_capsul_id(id)
completedProcess = run([join(current_app.root_path, 'shell_scripts/get.sh'), id], capture_output=True)
self.validate_completed_process(completedProcess)
lines = completedProcess.stdout.splitlines()
if len(lines) == 0:
ipaddr_lines = completedProcess.stdout.splitlines()
if len(ipaddr_lines) == 0:
return None
ipaddr = lines[0].decode("utf-8")
ipaddr = ipaddr_lines[0].decode("utf-8")
if not re.match(r"^([0-9]{1,3}\.){3}[0-9]{1,3}$", ipaddr):
return None
if get_ssh_host_keys:
try:
completedProcess2 = run([join(current_app.root_path, 'shell_scripts/ssh-keyscan.sh'), ipaddr], capture_output=True)
self.validate_completed_process(completedProcess2)
ssh_host_keys = json.loads(completedProcess2.stdout)
return VirtualMachine(id, ipv4=ipaddr, ssh_host_keys=ssh_host_keys)
except:
current_app.logger.warning(f"""
failed to ssh-keyscan {id} at {ipaddr}:
{my_exec_info_message(sys.exc_info())}"""
)
return VirtualMachine(id, ipv4=ipaddr)
def list_ids(self) -> list:
@ -111,13 +129,13 @@ class ShellScriptVirtualization(VirtualizationInterface):
self.validate_completed_process(completedProcess)
return list(map(lambda x: x.decode("utf-8"), completedProcess.stdout.splitlines() ))
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_authorized_keys: list):
validate_capsul_id(id)
if not re.match(r"^[a-zA-Z0-9/_.-]+$", template_image_file_name):
raise ValueError(f"template_image_file_name \"{template_image_file_name}\" must match \"^[a-zA-Z0-9/_.-]+$\"")
for ssh_public_key in ssh_public_keys:
for ssh_public_key in ssh_authorized_keys:
if not re.match(r"^(ssh|ecdsa)-[0-9A-Za-z+/_=@. -]+$", ssh_public_key):
raise ValueError(f"ssh_public_key \"{ssh_public_key}\" must match \"^(ssh|ecdsa)-[0-9A-Za-z+/_=@. -]+$\"")
@ -127,7 +145,7 @@ class ShellScriptVirtualization(VirtualizationInterface):
if memory_mb < 512 or memory_mb > 16384:
raise ValueError(f"memory_mb \"{memory_mb}\" must match 512 <= memory_mb <= 16384")
ssh_keys_string = "\n".join(ssh_public_keys)
ssh_keys_string = "\n".join(ssh_authorized_keys)
current_app.logger.info(f"create vm virt model ssh_keys_string: {ssh_keys_string}")
@ -149,7 +167,7 @@ class ShellScriptVirtualization(VirtualizationInterface):
template_image_file_name={template_image_file_name}
vcpus={str(vcpus)}
memory={str(memory_mb)}
ssh_public_keys={ssh_keys_string}
ssh_authorized_keys={ssh_keys_string}
"""
current_app.logger.info(f"create vm status: {status} vmSettings: {vmSettings}")