diff --git a/capsulflask/admin.py b/capsulflask/admin.py index 0cac03b..1352cca 100644 --- a/capsulflask/admin.py +++ b/capsulflask/admin.py @@ -3,7 +3,8 @@ import sys import json import ipaddress from datetime import datetime, timedelta -from flask import Blueprint, current_app, render_template, make_response +from flask import Blueprint, current_app, render_template, make_response, session, request, redirect, url_for +from flask_mail import Message from werkzeug.exceptions import abort from nanoid import generate @@ -18,11 +19,38 @@ bp = Blueprint("admin", __name__, url_prefix="/admin") @admin_account_required def index(): - # first create the hosts list w/ ip allocation visualization + 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) + + current_app.config["FLASK_MAIL_INSTANCE"].send( + Message( + request.form['subject'], + sender=current_app.config["MAIL_DEFAULT_SENDER"], + body=request.form['body'], + bcc=["forest.n.johnson@gmail.com", "forest@sequentialread.com"] + ) + ) + current_app.logger.info(f"sending email is done.") + return redirect(f"{url_for('admin.index')}") + else: + return abort(400, "unknown form action") + + + # first create the hosts list w/ ip allocation visualization from the database # - hosts = get_model().list_hosts_with_networks(None) - vms_by_host_and_network = get_model().non_deleted_vms_by_host_and_network(None) + db_hosts = get_model().list_hosts_with_networks(None) + db_vms_by_host_and_network = get_model().non_deleted_vms_by_host_and_network(None) network_display_width_px = float(270) #operations = get_model().list_all_operations() @@ -33,9 +61,9 @@ def index(): {'}'} """] - vm_by_id = dict() + db_vm_by_id = dict() - for kv in hosts.items(): + for kv in db_hosts.items(): host_id = kv[0] value = kv[1] display_host = dict(name=host_id, networks=value['networks']) @@ -48,13 +76,13 @@ def index(): network['allocations'] = [] network_addresses_width = float((network_end_int-network_start_int)+1) - if host_id in vms_by_host_and_network: - if network['network_name'] in vms_by_host_and_network[host_id]: - for vm in vms_by_host_and_network[host_id][network['network_name']]: + if host_id in db_vms_by_host_and_network: + if network['network_name'] in db_vms_by_host_and_network[host_id]: + for vm in db_vms_by_host_and_network[host_id][network['network_name']]: vm['network_name'] = network['network_name'] vm['virtual_bridge_name'] = network['virtual_bridge_name'] vm['host'] = host_id - vm_by_id[vm['id']] = vm + db_vm_by_id[vm['id']] = vm 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'])}" @@ -76,38 +104,43 @@ def index(): # Now creating the capsuls running status ui # - db_vms = get_model().all_vm_ids_with_desired_state() - # TODO will be replaced - #virt_vms = current_app.config["HUB_MODEL"].virsh_list() + virt_vms_by_host_and_network = current_app.config["HUB_MODEL"].get_all_by_host_and_network() - virt_vms_dict = dict() - for vm in virt_vms: - virt_vms_dict[vm["id"]] = vm["state"] + # virt_vms_dict = dict() + # for vm in virt_vms: + # virt_vms_dict[vm["id"]] = vm["state"] - in_db_but_not_in_virt = [] - needs_to_be_started = [] - needs_to_be_started_missing_ipv4 = [] + # in_db_but_not_in_virt = [] + # needs_to_be_started = [] + # needs_to_be_started_missing_ipv4 = [] + + # for vm in db_vms: + # if vm["id"] not in virt_vms_dict: + # in_db_but_not_in_virt.append(vm["id"]) + # elif vm["desired_state"] == "running" and virt_vms_dict[vm["id"]] != "running": + # if vm["id"] in db_vm_by_id: + # needs_to_be_started.append(db_vm_by_id[vm["id"]]) + # else: + # needs_to_be_started_missing_ipv4.append(vm["id"]) + # elif vm["ipv4"] != current_ipv4 + + # current_app.logger.info(f"list_of_networks: {json.dumps(list_of_networks)}") - for vm in db_vms: - if vm["id"] not in virt_vms_dict: - in_db_but_not_in_virt.append(vm["id"]) - elif vm["desired_state"] == "running" and virt_vms_dict[vm["id"]] != "running": - if vm["id"] in vm_by_id: - needs_to_be_started.append(vm_by_id[vm["id"]]) - else: - needs_to_be_started_missing_ipv4.append(vm["id"]) - elif vm["ipv4"] != current_ipv4 csp_inline_style_nonce = generate(alphabet="1234567890qwertyuiopasdfghjklzxcvbnm", size=10) response_text = render_template( "admin.html", + csrf_token=session["csrf-token"], display_hosts=display_hosts, - in_db_but_not_in_virt=in_db_but_not_in_virt, - needs_to_be_started=needs_to_be_started, - needs_to_be_started_missing_ipv4=needs_to_be_started_missing_ipv4, + # in_db_but_not_in_virt=in_db_but_not_in_virt, + # needs_to_be_started=needs_to_be_started, + # needs_to_be_started_missing_ipv4=needs_to_be_started_missing_ipv4, network_display_width_px=network_display_width_px, csp_inline_style_nonce=csp_inline_style_nonce, - inline_style='\n'.join(inline_styles) + inline_style='\n'.join(inline_styles), + + db_vms_by_host_and_network=json.dumps(db_vms_by_host_and_network), + virt_vms_by_host_and_network=json.dumps(virt_vms_by_host_and_network), ) response = make_response(response_text) diff --git a/capsulflask/db_model.py b/capsulflask/db_model.py index e1922ad..231c012 100644 --- a/capsulflask/db_model.py +++ b/capsulflask/db_model.py @@ -86,9 +86,9 @@ class DBModel: return hosts - # def all_vm_ids_with_desired_state(self): - # self.cursor.execute("SELECT id, desired_state FROM vms WHERE deleted IS NULL") - # return list(map(lambda x: {"id": x[0], "desired_state": x[1]}, self.cursor.fetchall())) + def all_accounts_with_active_vms(self): + self.cursor.execute("SELECT DISTINCT email FROM vms WHERE deleted IS NULL") + return list(map(lambda x: x[0], self.cursor.fetchall())) def operating_systems_dict(self): self.cursor.execute("SELECT id, template_image_file_name, description FROM os_images WHERE deprecated = FALSE") diff --git a/capsulflask/spoke_model.py b/capsulflask/spoke_model.py index f87536d..5b13030 100644 --- a/capsulflask/spoke_model.py +++ b/capsulflask/spoke_model.py @@ -39,7 +39,9 @@ class MockSpoke(VirtualizationInterface): return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4=ipv4, state="running") def get_all_by_host_and_network(self) -> dict: - return get_model().non_deleted_vms_by_host_and_network(None) + to_return = get_model().non_deleted_vms_by_host_and_network(None) + current_app.logger.info(f"MOCK get_all_by_host_and_network: {json.dumps(to_return)}") + return to_return def create(self, email: str, id: str, template_image_file_name: str, vcpus: int, memory_mb: int, ssh_authorized_keys: list, network_name: str, public_ipv4: str): validate_capsul_id(id) @@ -139,6 +141,8 @@ class ShellScriptSpoke(VirtualizationInterface): self.validate_completed_process(vm_list_process) list_of_vms = json.loads(vm_list_process.stdout.decode("utf-8")) + current_app.logger.info(f"list_of_vms: {json.dumps(list_of_vms)}") + vm_state_by_id = dict() for vm in list_of_vms: vm_state_by_id[vm['id']] = vm['state'] @@ -147,6 +151,8 @@ class ShellScriptSpoke(VirtualizationInterface): self.validate_completed_process(net_list_process) list_of_networks = json.loads(net_list_process.stdout.decode("utf-8")) + current_app.logger.info(f"list_of_networks: {json.dumps(list_of_networks)}") + networks = dict() vms_by_id = dict() vm_id_by_mac = dict() diff --git a/capsulflask/static/style.css b/capsulflask/static/style.css index 515f1a6..2877e22 100644 --- a/capsulflask/static/style.css +++ b/capsulflask/static/style.css @@ -378,3 +378,17 @@ footer { box-sizing: border-box; position: relative; } + +form.megaphone { + margin-top: 1rem; + width: 640px; + max-width: 100%; +} + +form.megaphone input[type=text], +form.megaphone textarea { + width: 100%; +} +form.megaphone textarea { + height: 360px; +} \ No newline at end of file diff --git a/capsulflask/templates/admin.html b/capsulflask/templates/admin.html index a52b02b..7b6d236 100644 --- a/capsulflask/templates/admin.html +++ b/capsulflask/templates/admin.html @@ -36,6 +36,43 @@
{% endfor %} + +
+

db_vms_by_host_and_network

+
+
+
+{{db_vms_by_host_and_network}}
+    
+
+ +
+

virt_vms_by_host_and_network

+
+
+
+{{virt_vms_by_host_and_network}}
+    
+
+ + + + + +
+
+

📢 Admin Megaphone: Email All Users With Active Capsuls 📢

+
+
+
+ + + + + +
+
+
{% endblock %}