first draft of full implementation with net_set_dhcp
This commit is contained in:
		@ -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">
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user