first draft of full implementation with net_set_dhcp
This commit is contained in:
parent
baee2f9178
commit
c7469f2acb
@ -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,18 +56,78 @@ 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"""
|
||||
@ -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),
|
||||
|
@ -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()
|
||||
|
@ -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}""")
|
@ -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}}$\"")
|
||||
|
15
capsulflask/shell_scripts/ip-dhcp-host.sh
Executable file
15
capsulflask/shell_scripts/ip-dhcp-host.sh
Executable 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
|
@ -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 = ""
|
||||
@ -222,4 +223,38 @@ def handle_vm_state_command(operation_id, request_body):
|
||||
current_app.logger.error(f"current_app.config['SPOKE_MODEL'].vm_state_command({params}) failed: {error_message}")
|
||||
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"))
|
@ -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):
|
||||
@ -298,4 +302,31 @@ class ShellScriptSpoke(VirtualizationInterface):
|
||||
self.validate_completed_process(completedProcess, email)
|
||||
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)
|
||||
|
@ -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">
|
||||
|
Loading…
Reference in New Issue
Block a user