Admin Megaphone: Email All Users With Active Capsuls 📢

This commit is contained in:
forest 2021-12-09 11:21:33 -06:00
parent e32ecd5ce2
commit b3f62018ea
5 changed files with 126 additions and 36 deletions

View File

@ -3,7 +3,8 @@ import sys
import json import json
import ipaddress import ipaddress
from datetime import datetime, timedelta 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 werkzeug.exceptions import abort
from nanoid import generate from nanoid import generate
@ -18,11 +19,38 @@ bp = Blueprint("admin", __name__, url_prefix="/admin")
@admin_account_required @admin_account_required
def index(): 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) db_hosts = get_model().list_hosts_with_networks(None)
vms_by_host_and_network = get_model().non_deleted_vms_by_host_and_network(None) db_vms_by_host_and_network = get_model().non_deleted_vms_by_host_and_network(None)
network_display_width_px = float(270) network_display_width_px = float(270)
#operations = get_model().list_all_operations() #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] host_id = kv[0]
value = kv[1] value = kv[1]
display_host = dict(name=host_id, networks=value['networks']) display_host = dict(name=host_id, networks=value['networks'])
@ -48,13 +76,13 @@ def index():
network['allocations'] = [] network['allocations'] = []
network_addresses_width = float((network_end_int-network_start_int)+1) network_addresses_width = float((network_end_int-network_start_int)+1)
if host_id in vms_by_host_and_network: if host_id in db_vms_by_host_and_network:
if network['network_name'] in vms_by_host_and_network[host_id]: if network['network_name'] in db_vms_by_host_and_network[host_id]:
for vm in vms_by_host_and_network[host_id][network['network_name']]: for vm in db_vms_by_host_and_network[host_id][network['network_name']]:
vm['network_name'] = network['network_name'] vm['network_name'] = network['network_name']
vm['virtual_bridge_name'] = network['virtual_bridge_name'] vm['virtual_bridge_name'] = network['virtual_bridge_name']
vm['host'] = host_id 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'])) ip_address_int = int(ipaddress.ip_address(vm['public_ipv4']))
if network_start_int <= ip_address_int and ip_address_int <= network_end_int: if network_start_int <= ip_address_int and ip_address_int <= network_end_int:
allocation = f"{host_id}_{network['network_name']}_{len(network['allocations'])}" allocation = f"{host_id}_{network['network_name']}_{len(network['allocations'])}"
@ -76,38 +104,43 @@ def index():
# Now creating the capsuls running status ui # Now creating the capsuls running status ui
# #
db_vms = get_model().all_vm_ids_with_desired_state() virt_vms_by_host_and_network = current_app.config["HUB_MODEL"].get_all_by_host_and_network()
# TODO will be replaced
#virt_vms = current_app.config["HUB_MODEL"].virsh_list()
virt_vms_dict = dict() # virt_vms_dict = dict()
for vm in virt_vms: # for vm in virt_vms:
virt_vms_dict[vm["id"]] = vm["state"] # virt_vms_dict[vm["id"]] = vm["state"]
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 = []
# 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) csp_inline_style_nonce = generate(alphabet="1234567890qwertyuiopasdfghjklzxcvbnm", size=10)
response_text = render_template( response_text = render_template(
"admin.html", "admin.html",
csrf_token=session["csrf-token"],
display_hosts=display_hosts, display_hosts=display_hosts,
in_db_but_not_in_virt=in_db_but_not_in_virt, # in_db_but_not_in_virt=in_db_but_not_in_virt,
needs_to_be_started=needs_to_be_started, # needs_to_be_started=needs_to_be_started,
needs_to_be_started_missing_ipv4=needs_to_be_started_missing_ipv4, # needs_to_be_started_missing_ipv4=needs_to_be_started_missing_ipv4,
network_display_width_px=network_display_width_px, network_display_width_px=network_display_width_px,
csp_inline_style_nonce=csp_inline_style_nonce, 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) response = make_response(response_text)

View File

@ -86,9 +86,9 @@ class DBModel:
return hosts return hosts
# def all_vm_ids_with_desired_state(self): def all_accounts_with_active_vms(self):
# self.cursor.execute("SELECT id, desired_state FROM vms WHERE deleted IS NULL") self.cursor.execute("SELECT DISTINCT email FROM vms WHERE deleted IS NULL")
# return list(map(lambda x: {"id": x[0], "desired_state": x[1]}, self.cursor.fetchall())) return list(map(lambda x: x[0], self.cursor.fetchall()))
def operating_systems_dict(self): def operating_systems_dict(self):
self.cursor.execute("SELECT id, template_image_file_name, description FROM os_images WHERE deprecated = FALSE") self.cursor.execute("SELECT id, template_image_file_name, description FROM os_images WHERE deprecated = FALSE")

View File

@ -39,7 +39,9 @@ class MockSpoke(VirtualizationInterface):
return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4=ipv4, state="running") return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4=ipv4, state="running")
def get_all_by_host_and_network(self) -> dict: 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): 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) validate_capsul_id(id)
@ -139,6 +141,8 @@ class ShellScriptSpoke(VirtualizationInterface):
self.validate_completed_process(vm_list_process) self.validate_completed_process(vm_list_process)
list_of_vms = json.loads(vm_list_process.stdout.decode("utf-8")) 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() vm_state_by_id = dict()
for vm in list_of_vms: for vm in list_of_vms:
vm_state_by_id[vm['id']] = vm['state'] vm_state_by_id[vm['id']] = vm['state']
@ -147,6 +151,8 @@ class ShellScriptSpoke(VirtualizationInterface):
self.validate_completed_process(net_list_process) self.validate_completed_process(net_list_process)
list_of_networks = json.loads(net_list_process.stdout.decode("utf-8")) 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() networks = dict()
vms_by_id = dict() vms_by_id = dict()
vm_id_by_mac = dict() vm_id_by_mac = dict()

View File

@ -378,3 +378,17 @@ footer {
box-sizing: border-box; box-sizing: border-box;
position: relative; 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;
}

View File

@ -36,6 +36,43 @@
<hr/> <hr/>
{% endfor %} {% endfor %}
<div class="row">
<h1>db_vms_by_host_and_network</h1>
</div>
<div class="row">
<pre>
{{db_vms_by_host_and_network}}
</pre>
</div>
<div class="row">
<h1>virt_vms_by_host_and_network</h1>
</div>
<div class="row">
<pre>
{{virt_vms_by_host_and_network}}
</pre>
</div>
</div>
<div class="third-margin">
<div class="row">
<h1>📢 Admin Megaphone: Email All Users With Active Capsuls 📢</h1>
</div>
<div class="row">
<form method="post" class="megaphone">
<input type="hidden" name="action" value="megaphone"></input>
<input type="hidden" name="csrf-token" value="{{ csrf_token }}"/>
<input type="text" name="subject" placeholder="Capsul Maintenance blahblahblah" />
<textarea name="body" placeholder="Hello, ..."></textarea>
<input type="submit" value="SEND"/>
</form>
</div>
</div> </div>
{% endblock %} {% endblock %}