capsul-flask/capsulflask/admin.py

260 lines
10 KiB
Python

import re
import sys
import json
import ipaddress
import pprint
from datetime import datetime, timedelta
from flask import Blueprint, current_app, render_template, make_response, session, request, redirect, url_for, flash
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 admin_account_required
from capsulflask.db import get_model
from capsulflask.consistency import get_all_vms_from_db, get_all_vms_from_hosts
from capsulflask.shared import my_exec_info_message
bp = Blueprint("admin", __name__, url_prefix="/admin")
@bp.route("/", methods=("GET", "POST"))
@admin_account_required
def index():
# these are always required to display the page anyways, so might as well
# grab them right off the bat as they are used inside the POST handler as well.
db_hosts = get_model().list_hosts_with_networks(None)
db_vms_by_id = get_all_vms_from_db()
virt_vms_by_id = get_all_vms_from_hosts()
network_display_width_px = float(270)
#operations = get_model().list_all_operations()
if request.method == "POST":
if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']:
return abort(418, f"u want tea")
if 'action' not in request.form:
return abort(400, "action is required")
if request.form['action'] == "megaphone":
emails_list = get_model().all_accounts_with_active_vms()
current_app.logger.info(f"sending '{request.form['subject']}' email to {len(emails_list)} users...")
# for email in emails_list:
# current_app.logger.info(email)
suffix1 = "This email was sent by the Capsul Admin Megaphone."
suffix2 = "If you have any questions DO NOT REPLY TO THIS EMAIL, direct your inquiry to support@cyberia.club"
current_app.config["FLASK_MAIL_INSTANCE"].send(
Message(
request.form['subject'],
sender=current_app.config["MAIL_DEFAULT_SENDER"],
body=f"{request.form['body']}\n\n{suffix1}\n{suffix2}",
bcc=emails_list,
)
)
current_app.logger.info(f"sending email is done.")
return redirect(f"{url_for('admin.index')}")
elif request.form['action'] == "start_or_stop":
if 'id' not in request.form:
return abort(400, "id is required")
if 'desired_state' not in request.form:
return abort(400, "desired_state is required")
id = request.form['id']
if id not in db_vms_by_id or id not in virt_vms_by_id:
return abort(404, "vm with that id was not found")
virt_vm = virt_vms_by_id[id]
db_vm = db_vms_by_id[id]
try:
if request.form['desired_state'] == "running":
if 'macs' in virt_vm and len(virt_vm['macs'].keys()) > 0:
current_app.config["HUB_MODEL"].net_set_dhcp(email=session['account'], host_id=virt_vm['host'], network_name=virt_vm['network_name'], macs=list(virt_vm['macs'].keys()), add_ipv4=db_vm['public_ipv4'])
current_app.config["HUB_MODEL"].vm_state_command(email=session['account'], id=id, command="start")
elif request.form['desired_state'] == "shut off":
current_app.config["HUB_MODEL"].vm_state_command(email=session['account'], id=id, command="force-stop")
else:
return abort(400, "desired_state must be either 'running' or 'shut off'")
except:
flash(f"""error during start_or_stop of {id}: {my_exec_info_message(sys.exc_info())}""")
return redirect(f"{url_for('admin.index')}")
elif request.form['action'] == "dhcp_reset":
if 'id' not in request.form:
return abort(400, "id is required")
id = request.form['id']
if id not in db_vms_by_id or id not in virt_vms_by_id:
return abort(404, "vm with that id was not found")
virt_vm = virt_vms_by_id[id]
db_vm = db_vms_by_id[id]
try:
current_app.config["HUB_MODEL"].vm_state_command(email=session['account'], id=id, command="force-stop")
current_app.config["HUB_MODEL"].net_set_dhcp(email=session['account'], host_id=virt_vm['host'], network_name=virt_vm['network_name'], macs=list(virt_vm['macs'].keys()), remove_ipv4=virt_vm['public_ipv4'], add_ipv4=db_vm['public_ipv4'])
current_app.config["HUB_MODEL"].vm_state_command(email=session['account'], id=id, command="start")
except:
flash(f"""error during dhcp_reset of {id}: {my_exec_info_message(sys.exc_info())}""")
return redirect(f"{url_for('admin.index')}")
elif request.form['action'] == "stop_and_expire":
if 'id' not in request.form:
return abort(400, "id is required")
id = request.form['id']
if id not in db_vms_by_id or id not in virt_vms_by_id:
return abort(404, "vm with that id was not found")
virt_vm = virt_vms_by_id[id]
#db_vm = db_vms_by_id[id]
current_app.config["HUB_MODEL"].vm_state_command(email=session['account'], id=id, command="force-stop")
current_app.config["HUB_MODEL"].net_set_dhcp(email=session['account'], host_id=virt_vm['host'], network_name=virt_vm['network_name'], macs=list(virt_vm['macs'].keys()), remove_ipv4=virt_vm['public_ipv4'])
return redirect(f"{url_for('admin.index')}")
else:
return abort(400, "unknown form action")
# moving on from the form post action stuff...
# first create the hosts list w/ ip allocation visualization from the database
#
display_hosts = []
inline_styles = [f"""
.network-display {'{'}
width: {network_display_width_px}px;
{'}'}
"""]
db_vms_by_host_network = dict()
for vm in db_vms_by_id.values():
host_network_key = f"{vm['host']}_{vm['network_name']}"
if host_network_key not in db_vms_by_host_network:
db_vms_by_host_network[host_network_key] = []
db_vms_by_host_network[host_network_key].append(vm)
for kv in db_hosts.items():
host_id = kv[0]
value = kv[1]
display_host = dict(name=host_id, networks=value['networks'])
for network in display_host['networks']:
network_start_int = int(ipaddress.ip_address(network["public_ipv4_first_usable_ip"]))
network_end_int = int(ipaddress.ip_address(network["public_ipv4_last_usable_ip"]))
network['allocations'] = []
network_addresses_width = float((network_end_int-network_start_int)+1)
host_network_key = f"{host_id}_{network['network_name']}"
if host_network_key in db_vms_by_host_network:
for vm in db_vms_by_host_network[host_network_key]:
ip_address_int = int(ipaddress.ip_address(vm['public_ipv4']))
if network_start_int <= ip_address_int and ip_address_int <= network_end_int:
allocation = f"{host_id}_{network['network_name']}_{len(network['allocations'])}"
inline_styles.append(
f"""
.{allocation} {'{'}
left: {(float(ip_address_int-network_start_int)/network_addresses_width)*network_display_width_px}px;
width: {network_display_width_px/network_addresses_width}px;
{'}'}
"""
)
network['allocations'].append(allocation)
else:
current_app.logger.warning(f"/admin: capsul {vm['id']} has public_ipv4 {vm['public_ipv4']} which is out of range for its host network {host_id} {network['network_name']} {network['public_ipv4_cidr_block']}")
display_hosts.append(display_host)
# Now creating the capsul consistency / running status ui
#
# current_app.logger.info(pprint.pformat(db_vms_by_id))
# current_app.logger.info("\n\n\n\n")
# current_app.logger.info(pprint.pformat(virt_vms_by_id))
# current_app.logger.info("\n\n\n\n")
virt_vm_id_by_ipv4 = dict()
for vm_id, virt_vm in virt_vms_by_id.items():
if 'public_ipv4' in virt_vm and virt_vm['public_ipv4'] != "":
virt_vm_id_by_ipv4[virt_vm['public_ipv4']] = vm_id
db_vm_id_by_ipv4 = dict()
for vm_id, db_vm in db_vms_by_id.items():
if 'public_ipv4' in db_vm and db_vm['public_ipv4'] != "":
db_vm_id_by_ipv4[db_vm['public_ipv4']] = vm_id
in_db_but_not_in_virt = []
state_not_equal_to_desired_state = []
has_no_desired_ip_address = []
has_not_aquired_ip_address_yet = []
stole_someone_elses_ip_and_own_ip_avaliable = []
stole_someone_elses_ip_but_own_ip_also_stolen = []
has_wrong_ip = []
for vm_id, db_vm in db_vms_by_id.items():
if vm_id not in virt_vms_by_id:
in_db_but_not_in_virt.append(db_vm)
elif virt_vms_by_id[vm_id]['state'] != db_vm["desired_state"]:
db_vm["state"] = virt_vms_by_id[vm_id]['state']
state_not_equal_to_desired_state.append(db_vm)
elif 'public_ipv4' not in db_vm or db_vm["public_ipv4"] == "":
has_no_desired_ip_address.append(db_vm)
elif db_vm["desired_state"] == "running" and 'public_ipv4' not in virt_vms_by_id[vm_id] or virt_vms_by_id[vm_id]['public_ipv4'] == "":
has_not_aquired_ip_address_yet.append(db_vm)
elif db_vm["desired_state"] == "running" and virt_vms_by_id[vm_id]['public_ipv4'] != db_vm["public_ipv4"]:
db_vm["desired_ipv4"] = db_vm["public_ipv4"]
db_vm["current_ipv4"] = virt_vms_by_id[vm_id]['public_ipv4']
if virt_vms_by_id[vm_id]['public_ipv4'] in db_vm_id_by_ipv4:
if db_vm["public_ipv4"] not in virt_vm_id_by_ipv4:
stole_someone_elses_ip_and_own_ip_avaliable.append(db_vm)
else:
stole_someone_elses_ip_but_own_ip_also_stolen.append(db_vm)
has_wrong_ip.append(db_vm)
# current_app.logger.info(f"list_of_networks: {json.dumps(list_of_networks)}")
csp_inline_style_nonce = generate(alphabet="1234567890qwertyuiopasdfghjklzxcvbnm", size=10)
response_text = render_template(
"admin.html",
csrf_token=session["csrf-token"],
display_hosts=display_hosts,
network_display_width_px=network_display_width_px,
csp_inline_style_nonce=csp_inline_style_nonce,
inline_style='\n'.join(inline_styles),
in_db_but_not_in_virt=in_db_but_not_in_virt,
state_not_equal_to_desired_state=state_not_equal_to_desired_state,
has_no_desired_ip_address=has_no_desired_ip_address,
has_not_aquired_ip_address_yet=has_not_aquired_ip_address_yet,
stole_someone_elses_ip_and_own_ip_avaliable=stole_someone_elses_ip_and_own_ip_avaliable,
stole_someone_elses_ip_but_own_ip_also_stolen=stole_someone_elses_ip_but_own_ip_also_stolen,
has_wrong_ip=has_wrong_ip
)
response = make_response(response_text)
response.headers.set('Content-Type', 'text/html')
response.headers.set('Content-Security-Policy', f"default-src 'self'; style-src 'self' 'nonce-{csp_inline_style_nonce}'")
return response