Admin Megaphone: Email All Users With Active Capsuls 📢
This commit is contained in:
parent
e32ecd5ce2
commit
b3f62018ea
@ -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)
|
||||||
|
@ -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")
|
||||||
|
@ -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()
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -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 %}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user