diff --git a/capsulflask/__init__.py b/capsulflask/__init__.py index c5de152..007cf05 100644 --- a/capsulflask/__init__.py +++ b/capsulflask/__init__.py @@ -42,6 +42,9 @@ app.config.from_mapping( SPOKE_HOST_ID=os.environ.get("SPOKE_HOST_ID", default="baikal"), SPOKE_HOST_TOKEN=os.environ.get("SPOKE_HOST_TOKEN", default="changeme"), HUB_TOKEN=os.environ.get("HUB_TOKEN", default="changeme"), + LIBVIRT_DNSMASQ_PATH=os.environ.get("LIBVIRT_DNSMASQ_PATH", default="/var/lib/libvirt/dnsmasq").rstrip("/"), + + # https://www.postgresql.org/docs/9.1/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS # https://stackoverflow.com/questions/56332906/where-to-put-ssl-certificates-when-trying-to-connect-to-a-remote-database-using diff --git a/capsulflask/hub_model.py b/capsulflask/hub_model.py index 4431a7f..3a8beb1 100644 --- a/capsulflask/hub_model.py +++ b/capsulflask/hub_model.py @@ -175,10 +175,11 @@ class CapsulFlaskHub(VirtualizationInterface): try: result_body = json.loads(result.body) - has_host = isinstance(result_body, dict) and host.id in result_body and isinstance(result_body[host.id], dict) - has_networks = has_host and 'networks' in result_body[host.id] and isinstance(result_body[host.id]['networks'], dict) - if has_host and has_networks: - to_return[host.id] = result_body[host.id]['networks'] + has_hosts = isinstance(result_body, dict) and "hosts" in result_body and isinstance(result_body["hosts"], dict) + has_current_host = has_hosts and isinstance(result_body["hosts"], dict) and host.id in result_body["hosts"] and isinstance(result_body["hosts"][host.id], dict) + has_networks = has_current_host and 'networks' in result_body["hosts"][host.id] and isinstance(result_body["hosts"][host.id]['networks'], dict) + if has_networks: + to_return[host.id] = result_body["hosts"][host.id]['networks'] else: # result_json_string = json.dumps({"error_message": "invalid response, missing 'networks' list"}) current_app.logger.error(f"""missing 'networks' list for get_all_by_host_and_network operation, host {host.id}""") diff --git a/capsulflask/shell_scripts/virsh-list.sh b/capsulflask/shell_scripts/virsh-list.sh index 11147c1..9e48fa0 100755 --- a/capsulflask/shell_scripts/virsh-list.sh +++ b/capsulflask/shell_scripts/virsh-list.sh @@ -2,12 +2,10 @@ printf '[' delimiter="" -virsh list --all | while read -r line; do - if echo "$line" | grep -qE '(running)|(shut off)'; then - capsul_id="$(echo "$line" | awk '{ print $2 }')" - capsul_state="$(echo "$line" | sed -E 's/.*((running)|(shut off))\w*/\1/')" - printf '%s\n {"id":"%s", "state":"%s"}' "$delimiter" "$capsul_id" "$capsul_state" - delimiter="," - fi +virsh list --all | tail -n +3 | while read -r line; do + capsul_id="$(echo "$line" | awk '{ print $2 }')" + capsul_state="$(echo "$line" | sed -E 's/^ *[0-9-]+ +[^ ]+ +//')" + printf '%s\n {"id":"%s", "state":"%s"}' "$delimiter" "$capsul_id" "$capsul_state" + delimiter="," done printf '\n]\n' diff --git a/capsulflask/shell_scripts/virsh-net-list.sh b/capsulflask/shell_scripts/virsh-net-list.sh index 65b47c8..e7c4e79 100644 --- a/capsulflask/shell_scripts/virsh-net-list.sh +++ b/capsulflask/shell_scripts/virsh-net-list.sh @@ -1,3 +1,13 @@ #!/bin/sh -virsh net-list --all | tail -n +3 | awk '{ print $1 }' \ No newline at end of file + +printf '[' +delimiter="" +virsh net-list --all | tail -n +3 | awk '{ print $1 }' | while read -r network_name; do + virtual_bridge_name="$(virsh net-info "$network_name" | grep -E '^Bridge:' | awk '{ print $2 }')" + capsul_state="$(echo "$line" | sed -E 's/^ *[0-9-]+ +[^ ]+ +//')" + printf '%s\n {"name":"%s", "virtual_bridge_name":"%s"}' "$delimiter" "$network_name" "$virtual_bridge_name" + delimiter="," +done +printf '\n]\n' + diff --git a/capsulflask/spoke_api.py b/capsulflask/spoke_api.py index 54af3a2..35733a4 100644 --- a/capsulflask/spoke_api.py +++ b/capsulflask/spoke_api.py @@ -96,7 +96,7 @@ def handle_get(operation_id, request_body): return jsonify(dict(assignment_status="assigned", id=vm.id, host=vm.host, state=vm.state, ipv4=vm.ipv4, ipv6=vm.ipv6, ssh_host_keys=vm.ssh_host_keys)) -def handle_get_all_by_host_and_network(operation_id, request_body): +def handle_get_all_by_host_and_network(operation_id, request_body) -> dict: return jsonify(dict(assignment_status="assigned", ids=current_app.config['SPOKE_MODEL'].get_all_by_host_and_network())) def handle_create(operation_id, request_body): diff --git a/capsulflask/spoke_model.py b/capsulflask/spoke_model.py index e38aef9..f87536d 100644 --- a/capsulflask/spoke_model.py +++ b/capsulflask/spoke_model.py @@ -133,11 +133,62 @@ class ShellScriptSpoke(VirtualizationInterface): return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], state=state, ipv4=ipaddr) - def get_all_by_host_and_network(self) -> list: - # TODO implement this - completedProcess = run([join(current_app.root_path, 'shell_scripts/virsh-list.sh')], capture_output=True) - self.validate_completed_process(completedProcess) - return json.loads(completedProcess.stdout.decode("utf-8")) + def get_all_by_host_and_network(self) -> dict: + + vm_list_process = run([join(current_app.root_path, 'shell_scripts/virsh-list.sh')], capture_output=True) + self.validate_completed_process(vm_list_process) + list_of_vms = json.loads(vm_list_process.stdout.decode("utf-8")) + + vm_state_by_id = dict() + for vm in list_of_vms: + vm_state_by_id[vm['id']] = vm['state'] + + net_list_process = run([join(current_app.root_path, 'shell_scripts/virsh-net-list.sh')], capture_output=True) + self.validate_completed_process(net_list_process) + list_of_networks = json.loads(net_list_process.stdout.decode("utf-8")) + + networks = dict() + vms_by_id = dict() + vm_id_by_mac = dict() + for network in list_of_networks: + + with open(f"{current_app.config['LIBVIRT_DNSMASQ_PATH']}/{network['virtual_bridge_name']}.macs", mode='r') as macs_json_file: + vms_with_macs = json.load(macs_json_file) + for vm in vms_with_macs: + for mac in vm['macs']: + if mac not in vm_id_by_mac: + vm_id_by_mac[mac] = vm['domain'] + else: + raise Exception(f"the mac address '{mac}' is used by both '{vm_id_by_mac[mac]}' and '{vm['domain']}'") + + if vm['domain'] not in vms_by_id: + vm_state = 'shut off' + if vm['domain'] in vm_state_by_id: + vm_state = vm_state_by_id[vm['domain']] + else: + current_app.logger.info(f"get_all_by_host_and_network: '{vm['domain']}' not in vm_state_by_id, defaulting to 'shut off'") + + vms_by_id[vm['domain']] = dict(macs=dict(), state=vm_state) + + vms_by_id[vm['domain']]['macs'][mac] = True + + with open(f"{current_app.config['LIBVIRT_DNSMASQ_PATH']}/{network['virtual_bridge_name']}.status", mode='r') as status_json_file: + statuses = json.load(status_json_file) + for status in statuses: + if status['mac-address'] in vm_id_by_mac: + vm_id = vm_id_by_mac[status['mac-address']] + vms_by_id[vm_id]['ipv4'] = status['ip-address'] + else: + current_app.logger.info(f"get_all_by_host_and_network: {status['mac-address']} not in vm_id_by_mac") + + networks[network['name']] = vms_by_id + + to_return = dict() + to_return[current_app.config['SPOKE_HOST_ID']] = networks + + 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): validate_capsul_id(id)