first draft of full implementation with net_set_dhcp

This commit is contained in:
forest 2021-12-09 17:25:44 -06:00
parent baee2f9178
commit c7469f2acb
8 changed files with 197 additions and 15 deletions

View File

@ -4,7 +4,7 @@ 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
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
@ -21,6 +21,15 @@ bp = Blueprint("admin", __name__, url_prefix="/admin")
@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")
@ -47,19 +56,79 @@ def index():
)
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=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=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=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 stuff...
# moving on from the form post action stuff...
# first create the hosts list w/ ip allocation visualization from the database
#
db_hosts = get_model().list_hosts_with_networks(None)
db_vms_by_id = get_all_vms_from_db()
network_display_width_px = float(270)
#operations = get_model().list_all_operations()
display_hosts = []
inline_styles = [f"""
.network-display {'{'}
@ -112,7 +181,7 @@ def index():
# Now creating the capsul consistency / running status ui
#
virt_vms_by_id = get_all_vms_from_hosts()
# current_app.logger.info(pprint.pformat(db_vms_by_id))
@ -170,9 +239,6 @@ def index():
"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,
network_display_width_px=network_display_width_px,
csp_inline_style_nonce=csp_inline_style_nonce,
inline_style='\n'.join(inline_styles),

View File

@ -434,6 +434,14 @@ class DBModel:
)
self.connection.commit()
def host_by_id(self, host_id: str) -> OnlineHost:
self.cursor.execute("SELECT hosts.id, hosts.https_url FROM hosts hosts.id = %s", (host_id,))
row = self.cursor.fetchone()
if row:
return OnlineHost(row[0], row[1])
else:
return None
def host_of_capsul(self, capsul_id: str) -> OnlineHost:
self.cursor.execute("SELECT hosts.id, hosts.https_url from vms JOIN hosts on hosts.id = vms.host where vms.id = %s", (capsul_id,))
row = self.cursor.fetchone()

View File

@ -61,6 +61,9 @@ class MockHub(VirtualizationInterface):
def vm_state_command(self, email: str, id: str, command: str):
current_app.logger.info(f"mock {command}: {id} for {email}")
def net_set_dhcp(self, email: str, host_id: str, network_name: str, macs: list, remove_ipv4: str, add_ipv4: str):
current_app.logger.info(f"mock net_set_dhcp: host_id={host_id} network_name={network_name} macs={','.join(macs)} remove_ipv4={remove_ipv4} add_ipv4={add_ipv4} for {email}")
class CapsulFlaskHub(VirtualizationInterface):
def synchronous_operation(self, hosts: List[OnlineHost], email: str, payload: str) -> List[HTTPResult]:
@ -270,3 +273,23 @@ class CapsulFlaskHub(VirtualizationInterface):
if not result_status == "success":
raise ValueError(f"""failed to {command} vm "{id}" on host "{host.id}" for {email}: {result_json_string}""")
def net_set_dhcp(self, email: str, host_id: str, network_name: str, macs: list, remove_ipv4: str, add_ipv4: str):
validate_capsul_id(id)
result_status = None
host = get_model().host_by_id(host_id)
if host is not None:
payload = json.dumps(dict(type="net_set_dhcp", network_name=network_name, macs=macs, remove_ipv4=remove_ipv4, add_ipv4=add_ipv4))
results = self.synchronous_operation([host], email, payload)
result_json_string = "<no response from host>"
for result in results:
try:
result_json_string = result.body
result_body = json.loads(result_json_string)
if isinstance(result_body, dict) and 'status' in result_body:
result_status = result_body['status']
except:
pass
if not result_status == "success":
raise ValueError(f"""failed to net_set_dhcp on host "{host.id}" for {email}: {result_json_string}""")

View File

@ -44,6 +44,10 @@ class VirtualizationInterface:
def vm_state_command(self, email: str, id: str, command: str):
pass
def net_set_dhcp(self, email: str, host_id: str, network_name: str, macs: list, remove_ipv4: str, add_ipv4: str):
pass
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}}$\"")

View File

@ -0,0 +1,15 @@
#!/bin/sh -e
#
# ip-dhcp-host.sh - add or remove a mac address --> ipv4 mapping
action="$1"
network_name="$2"
mac_address="$3"
ipv4_address="$4"
[ "$action" != 'add' ] && [ "$action" != 'delete' ] && printf 'you must set $action to either add or delete (1st arg)\n' && exit 1
[ "$network_name" = '' ] && printf 'you must set $network_name (2nd arg)\n' && exit 1
[ "$mac_address" = '' ] && printf 'you must set $mac_address (3rd arg)\n' && exit 1
[ "$ipv4_address" = '' ] && printf 'you must set $ipv4_address (4th arg)\n' && exit 1
virsh net-update "$network_name" "$action" ip-dhcp-host "<host mac='$mac_address' ip='$ipv4_address' />" --live --config

View File

@ -53,6 +53,7 @@ def operation_impl(operation_id: int):
"create": handle_create,
"destroy": handle_destroy,
"vm_state_command": handle_vm_state_command,
"net_set_dhcp": handle_net_set_dhcp,
}
error_message = ""
@ -223,3 +224,37 @@ def handle_vm_state_command(operation_id, request_body):
return jsonify(dict(assignment_status="assigned", status="error", error_message=error_message))
return jsonify(dict(assignment_status="assigned", status="success"))
def handle_net_set_dhcp(operation_id, request_body):
required_properties = ['network_name', 'macs']
for required_property in required_properties:
if required_property not in request_body:
current_app.logger.error(f"/hosts/operation returned 400: {required_property} is required for net_set_dhcp")
return abort(400, f"bad request; {required_property} is required for net_set_dhcp")
remove_is_missing = ('remove_ipv4' not in request_body or request_body['remove_ipv4'] is None)
add_is_missing = ('add_ipv4' not in request_body or request_body['add_ipv4'] is None)
if remove_is_missing and add_is_missing:
current_app.logger.error(f"/hosts/operation returned 400: either remove_ipv4 or add_ipv4 is required for net_set_dhcp")
return abort(400, f"bad request; either remove_ipv4 or add_ipv4 is required for net_set_dhcp")
if remove_is_missing:
request_body['remove_ipv4'] = None
if add_is_missing:
request_body['add_ipv4'] = None
try:
current_app.config['SPOKE_MODEL'].net_set_dhcp(email=request_body['email'], network_name=request_body['network_name'], macs=request_body['macs'], remove_ipv4=request_body['remove_ipv4'], add_ipv4=request_body['add_ipv4'])
except:
error_message = my_exec_info_message(sys.exc_info())
params= f"email='{request_body['email'] if 'email' in request_body else 'KeyError'}', "
params= f"{params} network_name='{request_body['network_name'] if 'network_name' in request_body else 'KeyError'}', "
params= f"{params} macs='{request_body['macs'] if 'macs' in request_body else 'KeyError'}', "
params= f"{params} remove_ipv4='{request_body['remove_ipv4'] if 'remove_ipv4' in request_body else 'KeyError'}', "
params= f"{params} add_ipv4='{request_body['add_ipv4'] if 'add_ipv4' in request_body else 'KeyError'}', "
current_app.logger.error(f"current_app.config['SPOKE_MODEL'].net_set_dhcp({params}) failed: {error_message}")
return jsonify(dict(assignment_status="assigned", status="error", error_message=error_message))
return jsonify(dict(assignment_status="assigned", status="success"))

View File

@ -56,6 +56,10 @@ class MockSpoke(VirtualizationInterface):
def vm_state_command(self, email: str, id: str, command: str):
current_app.logger.info(f"mock {command}: {id} for {email}")
def net_set_dhcp(self, email: str, host_id: str, network_name: str, macs: list, remove_ipv4: str, add_ipv4: str):
current_app.logger.info(f"mock net_set_dhcp: host_id={host_id} network_name={network_name} macs={','.join(macs)} remove_ipv4={remove_ipv4} add_ipv4={add_ipv4} for {email}")
class ShellScriptSpoke(VirtualizationInterface):
def validate_completed_process(self, completedProcess, email=None):
@ -299,3 +303,30 @@ class ShellScriptSpoke(VirtualizationInterface):
returned_string = completedProcess.stdout.decode("utf-8")
current_app.logger.info(f"{command} vm {id} for {email} returned: {returned_string}")
def net_set_dhcp(self, email: str, host_id: str, network_name: str, macs: list, remove_ipv4: str, add_ipv4: str):
if not re.match(r"^[a-zA-Z0-9_-]+$", network_name):
raise ValueError(f"network_name \"{network_name}\" must match \"^[a-zA-Z0-9_-]+\"")
if not isinstance(macs, list):
raise ValueError(f"macs must be a list")
for mac in macs:
if not re.match(r"^[0-9a-f:]+$", mac):
raise ValueError(f"mac \"{mac}\" must match \"^[0-9a-f:]+$\"")
if remove_ipv4 != None and remove_ipv4 != "":
if not re.match(r"^[0-9.]+$", remove_ipv4):
raise ValueError(f"remove_ipv4 \"{remove_ipv4}\" must match \"^[0-9.]+$\"")
for mac in macs:
completedProcess = run([join(current_app.root_path, f"shell_scripts/ip-dhcp-host.sh"), "delete", network_name, mac, remove_ipv4], capture_output=True)
self.validate_completed_process(completedProcess, email)
if add_ipv4 != None and add_ipv4 != "":
if not re.match(r"^[0-9.]+$", add_ipv4):
raise ValueError(f"add_ipv4 \"{add_ipv4}\" must match \"^[0-9.]+$\"")
for mac in macs:
completedProcess = run([join(current_app.root_path, f"shell_scripts/ip-dhcp-host.sh"), "add", network_name, mac, add_ipv4], capture_output=True)
self.validate_completed_process(completedProcess, email)

View File

@ -60,9 +60,9 @@
<div class="row">
<div>{{vm['id']}} ({{vm['email']}}): state={{vm['state']}} desired_state={{vm['desired_state']}}</div>
<form method="post">
<input type="hidden" name="action" value="set_state"></input>
<input type="hidden" name="action" value="start_or_stop"></input>
<input type="hidden" name="id" value="{{vm['id']}}"></input>
<input type="hidden" name="state" value="{{vm['desired_state']}}"></input>
<input type="hidden" name="desired_state" value="{{vm['desired_state']}}"></input>
<input type="hidden" name="csrf-token" value="{{ csrf_token }}"/>
<input type="submit" value="🚦 START/STOP"/>
</form>
@ -154,7 +154,7 @@
<div class="third-margin">
<div class="row">
<h1>📢 Admin Megaphone: Email All Users With Active Capsuls 📢</h1>
<h1>📢 Admin Megaphone: Email All Users who have Active Capsuls 📢</h1>
</div>
<div class="row">
<form method="post" class="megaphone">