capsul.org webapp

working on support for managing VM state and IP address

forest cc349d26 9d74c99c

+34
capsulflask/admin.py
··· 17 17 @bp.route("/") 18 18 @admin_account_required 19 19 def index(): 20 + 21 + # first create the hosts list w/ ip allocation visualization 22 + # 23 + 20 24 hosts = get_model().list_hosts_with_networks(None) 21 25 vms_by_host_and_network = get_model().non_deleted_vms_by_host_and_network(None) 22 26 network_display_width_px = float(270) ··· 29 33 {'}'} 30 34 """] 31 35 36 + public_ipv4_by_capsul_id = dict() 37 + 32 38 for kv in hosts.items(): 33 39 host_id = kv[0] 34 40 value = kv[1] ··· 45 51 if host_id in vms_by_host_and_network: 46 52 if network['network_name'] in vms_by_host_and_network[host_id]: 47 53 for vm in vms_by_host_and_network[host_id][network['network_name']]: 54 + public_ipv4_by_capsul_id[vm['id']] = vm['public_ipv4'] 48 55 ip_address_int = int(ipaddress.ip_address(vm['public_ipv4'])) 49 56 if network_start_int <= ip_address_int and ip_address_int <= network_end_int: 50 57 allocation = f"{host_id}_{network['network_name']}_{len(network['allocations'])}" ··· 62 69 63 70 display_hosts.append(display_host) 64 71 72 + 73 + # Now creating the capsuls running status ui 74 + # 75 + 76 + db_vms = get_model().all_vm_ids_with_desired_state() 77 + virt_vms = current_app.config["HUB_MODEL"].list_ids_with_desired_state() 78 + 79 + virt_vms_dict = dict() 80 + for vm in virt_vms: 81 + virt_vms_dict[vm["id"]] = vm["state"] 82 + 83 + in_db_but_not_in_virt = [] 84 + needs_to_be_started = [] 85 + needs_to_be_started_missing_ipv4 = [] 86 + 87 + for vm in db_vms: 88 + if vm["id"] not in virt_vms_dict: 89 + in_db_but_not_in_virt.append(vm["id"]) 90 + elif vm["desired_state"] == "running" and virt_vms_dict[vm["id"]] != "running": 91 + if vm["id"] in public_ipv4_by_capsul_id: 92 + needs_to_be_started.append({"id": vm["id"], "ipv4": public_ipv4_by_capsul_id[vm["id"]]}) 93 + else: 94 + needs_to_be_started_missing_ipv4.append(vm["id"]) 95 + 65 96 csp_inline_style_nonce = generate(alphabet="1234567890qwertyuiopasdfghjklzxcvbnm", size=10) 66 97 response_text = render_template( 67 98 "admin.html", 68 99 display_hosts=display_hosts, 100 + in_db_but_not_in_virt=in_db_but_not_in_virt, 101 + needs_to_be_started=needs_to_be_started, 102 + needs_to_be_started_missing_ipv4=needs_to_be_started_missing_ipv4, 69 103 network_display_width_px=network_display_width_px, 70 104 csp_inline_style_nonce=csp_inline_style_nonce, 71 105 inline_style='\n'.join(inline_styles)
+8 -6
capsulflask/cli.py
··· 268 268 269 269 270 270 def ensure_vms_and_db_are_synced(): 271 - db_ids = get_model().all_non_deleted_vm_ids() 272 - virt_ids = current_app.config["HUB_MODEL"].list_ids() 271 + db_vms = get_model().all_vm_ids_with_desired_state() 272 + virt_vms = current_app.config["HUB_MODEL"].list_ids_with_desired_state() 273 273 274 274 db_ids_dict = dict() 275 275 virt_ids_dict = dict() 276 276 277 - for id in db_ids: 278 - db_ids_dict[id] = True 277 + for vm in db_vms: 278 + db_ids_dict[vm['id']] = vm['desired_state'] 279 279 280 - for id in virt_ids: 281 - virt_ids_dict[id] = True 280 + for vm in virt_vms: 281 + virt_ids_dict[vm['id']] = vm['desired_state'] 282 282 283 283 errors = list() 284 284 285 285 for id in db_ids_dict: 286 286 if id not in virt_ids_dict: 287 287 errors.append(f"{id} is in the database but not in the virtualization model") 288 + elif db_ids_dict[id] != virt_ids_dict[id]: 289 + errors.append(f"{id} has the desired state {db_ids_dict[id]} in the database but current state {virt_ids_dict[id]} in the virtualization model") 288 290 289 291 for id in virt_ids_dict: 290 292 if id not in db_ids_dict:
+108
capsulflask/consistency.py
··· 1 + 2 + from flask import current_app 3 + from capsulflask.db import get_model 4 + 5 + # { 6 + # "capsul-123abc45": { 7 + # "id": "capsul-123abc45", 8 + # "public_ipv4": "123.123.123.123", 9 + # "public_ipv6": "::::", 10 + # "host": "baikal", 11 + # "network_name": "public1", 12 + # "virtual_bridge_name": "virbr1", 13 + # "state": "running" 14 + # }, 15 + # { ... }, 16 + # ... 17 + # } 18 + def get_all_vms_from_db() -> dict: 19 + db_hosts = get_model().list_hosts_with_networks(None) 20 + db_vms_by_host_and_network = get_model().non_deleted_vms_by_host_and_network(None) 21 + 22 + db_vms_by_id = dict() 23 + 24 + for kv in db_hosts.items(): 25 + host_id = kv[0] 26 + value = kv[1] 27 + for network in value['networks']: 28 + if host_id in db_vms_by_host_and_network and network['network_name'] in db_vms_by_host_and_network[host_id]: 29 + for vm in db_vms_by_host_and_network[host_id][network['network_name']]: 30 + vm['network_name'] = network['network_name'] 31 + vm['virtual_bridge_name'] = network['virtual_bridge_name'] 32 + vm['host'] = host_id 33 + db_vms_by_id[vm['id']] = vm 34 + 35 + # for vm in db_vms: 36 + # if vm["id"] not in db_vms_by_id: 37 + # # TODO 38 + # raise Exception("non_deleted_vms_by_host_and_network did not return a vm that was returned by all_vm_ids_with_desired_state") 39 + # else: 40 + # db_vms_by_id[vm["id"]]["state"] = vm["desired_state"] 41 + 42 + return db_vms_by_id 43 + 44 + def get_all_vms_from_hosts() -> dict: 45 + virt_vms = current_app.config["HUB_MODEL"].virsh_list() 46 + virt_networks = current_app.config["HUB_MODEL"].virsh_netlist() 47 + db_hosts = get_model().list_hosts_with_networks(None) 48 + 49 + virt_vms_by_id = dict() 50 + 51 + for kv in db_hosts.items(): 52 + host_id = kv[0] 53 + value = kv[1] 54 + for network in value['networks']: 55 + 56 + 57 + for vm in db_vms: 58 + if vm["id"] not in db_vms_by_id: 59 + # TODO 60 + raise Exception("non_deleted_vms_by_host_and_network did not return a vm that was returned by all_vm_ids_with_desired_state") 61 + else: 62 + db_vms_by_id[vm["id"]]["state"] = vm["desired_state"] 63 + 64 + virt_vms = current_app.config["HUB_MODEL"].get_vm_() 65 + 66 + def ensure_vms_and_db_are_synced(): 67 + 68 + 69 + 70 + # Now creating the capsuls running status ui 71 + # 72 + 73 + 74 + 75 + for vm in db_vms: 76 + db_ids_dict[vm['id']] = vm['desired_state'] 77 + 78 + for vm in virt_vms: 79 + virt_ids_dict[vm['id']] = vm['desired_state'] 80 + 81 + errors = list() 82 + 83 + for id in db_ids_dict: 84 + if id not in virt_ids_dict: 85 + errors.append(f"{id} is in the database but not in the virtualization model") 86 + elif db_ids_dict[id] != virt_ids_dict[id]: 87 + errors.append(f"{id} has the desired state {db_ids_dict[id]} in the database but current state {virt_ids_dict[id]} in the virtualization model") 88 + 89 + for id in virt_ids_dict: 90 + if id not in db_ids_dict: 91 + errors.append(f"{id} is in the virtualization model but not in the database") 92 + 93 + if len(errors) > 0: 94 + email_addresses_raw = current_app.config['ADMIN_EMAIL_ADDRESSES'].split(",") 95 + email_addresses = list(filter(lambda x: len(x) > 6, map(lambda x: x.strip(), email_addresses_raw ) )) 96 + 97 + current_app.logger.info(f"cron_task: sending inconsistency warning email to {','.join(email_addresses)}:") 98 + for error in errors: 99 + current_app.logger.info(f"cron_task: {error}.") 100 + 101 + current_app.config["FLASK_MAIL_INSTANCE"].send( 102 + Message( 103 + "Capsul Consistency Check Failed", 104 + sender=current_app.config["MAIL_DEFAULT_SENDER"], 105 + body="\n".join(errors), 106 + recipients=email_addresses 107 + ) 108 + )
+6 -6
capsulflask/console.py
··· 27 27 letters_n_nummers = generate(alphabet="1234567890qwertyuiopasdfghjklzxcvbnm", size=10) 28 28 return f"capsul-{letters_n_nummers}" 29 29 30 - def double_check_capsul_address(id, ipv4, get_ssh_host_keys): 30 + def double_check_capsul_address(id, get_ssh_host_keys): 31 31 try: 32 32 result = current_app.config["HUB_MODEL"].get(id, get_ssh_host_keys) 33 - if result != None and result.ipv4 != None and result.ipv4 != ipv4: 34 - ipv4 = result.ipv4 35 - get_model().update_vm_ip(email=session["account"], id=id, ipv4=result.ipv4) 33 + # if result != None and result.ipv4 != None and result.ipv4 != ipv4: 34 + # ipv4 = result.ipv4 35 + # get_model().update_vm_ip(email=session["account"], id=id, ipv4=result.ipv4) 36 36 37 37 if result != None and result.ssh_host_keys != None and get_ssh_host_keys: 38 38 get_model().update_vm_ssh_host_keys(email=session["account"], id=id, ssh_host_keys=result.ssh_host_keys) ··· 59 59 # for now we are going to check the IP according to the virt model 60 60 # on every request. this could be done by a background job and cached later on... 61 61 for vm in vms: 62 - result = double_check_capsul_address(vm["id"], vm["ipv4"], False) 62 + result = double_check_capsul_address(vm["id"], False) 63 63 if result is not None: 64 64 vm["ipv4"] = result.ipv4 65 65 vm["state"] = result.state ··· 167 167 else: 168 168 needs_ssh_host_keys = "ssh_host_keys" not in vm or len(vm["ssh_host_keys"]) == 0 169 169 170 - vm_from_virt_model = double_check_capsul_address(vm["id"], vm["ipv4"], needs_ssh_host_keys) 170 + vm_from_virt_model = double_check_capsul_address(vm["id"], needs_ssh_host_keys) 171 171 172 172 if vm_from_virt_model is not None: 173 173 vm["ipv4"] = vm_from_virt_model.ipv4
+1 -1
capsulflask/db.py
··· 50 50 hasSchemaVersionTable = False 51 51 actionWasTaken = False 52 52 schemaVersion = 0 53 - desiredSchemaVersion = 20 53 + desiredSchemaVersion = 21 54 54 55 55 cursor = connection.cursor() 56 56
+9 -8
capsulflask/db_model.py
··· 86 86 87 87 return hosts 88 88 89 - def all_non_deleted_vm_ids(self): 90 - self.cursor.execute("SELECT id FROM vms WHERE deleted IS NULL") 91 - return list(map(lambda x: x[0], self.cursor.fetchall())) 89 + def all_vm_ids_with_desired_state(self): 90 + self.cursor.execute("SELECT id, desired_state FROM vms WHERE deleted IS NULL") 91 + return list(map(lambda x: {"id": x[0], "desired_state": x[1]}, self.cursor.fetchall())) 92 92 93 93 def operating_systems_dict(self): 94 94 self.cursor.execute("SELECT id, template_image_file_name, description FROM os_images WHERE deprecated = FALSE") ··· 332 332 333 333 def list_hosts_with_networks(self, host_id: str): 334 334 query = """ 335 - SELECT hosts.id, hosts.last_health_check, host_network.network_name, 335 + SELECT hosts.id, hosts.last_health_check, host_network.network_name, host_network.virtual_bridge_name, 336 336 host_network.public_ipv4_cidr_block, host_network.public_ipv4_first_usable_ip, host_network.public_ipv4_last_usable_ip 337 337 FROM hosts 338 338 JOIN host_network ON host_network.host = hosts.id ··· 354 354 hosts[row[0]] = dict(last_health_check=row[1], networks=[]) 355 355 356 356 hosts[row[0]]["networks"].append(dict( 357 - network_name=row[2], 358 - public_ipv4_cidr_block=row[3], 359 - public_ipv4_first_usable_ip=row[4], 360 - public_ipv4_last_usable_ip=row[5] 357 + network_name=row[2], 358 + virtual_bridge_name=row[3], 359 + public_ipv4_cidr_block=row[4], 360 + public_ipv4_first_usable_ip=row[5], 361 + public_ipv4_last_usable_ip=row[6] 361 362 )) 362 363 363 364 return hosts
+12 -17
capsulflask/hub_model.py
··· 37 37 38 38 return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4=self.default_ipv4) 39 39 40 - def list_ids(self) -> list: 41 - return get_model().all_non_deleted_vm_ids() 40 + def get_all_by_host_and_network(self) -> dict: 41 + return get_model().non_deleted_vms_by_host_and_network() 42 42 43 43 def create(self, email: str, id: str, os: str, size: str, template_image_file_name: str, vcpus: int, memory_mb: int, ssh_authorized_keys: list): 44 44 validate_capsul_id(id) ··· 164 164 165 165 return None 166 166 167 - def list_ids(self) -> list: 167 + def get_all_by_host_and_network(self) -> dict: 168 168 online_hosts = get_model().get_online_hosts() 169 - payload = json.dumps(dict(type="list_ids")) 169 + payload = json.dumps(dict(type="get_all_by_host_and_network")) 170 170 results = self.synchronous_operation(online_hosts, None, payload) 171 - to_return = [] 171 + to_return = dict() 172 172 for i in range(len(results)): 173 173 host = online_hosts[i] 174 174 result = results[i] 175 175 try: 176 176 result_body = json.loads(result.body) 177 - if isinstance(result_body, dict) and 'ids' in result_body and isinstance(result_body['ids'], list): 178 - all_valid = True 179 - for id in result_body['ids']: 180 - try: 181 - validate_capsul_id(id) 182 - to_return.append(id) 183 - except: 184 - all_valid = False 185 - if not all_valid: 186 - current_app.logger.error(f"""error reading ids for list_ids operation, host {host.id}""") 177 + 178 + has_host = isinstance(result_body, dict) and host.id in result_body and isinstance(result_body[host.id], dict) 179 + has_networks = has_host and 'networks' in result_body[host.id] and isinstance(result_body[host.id]['networks'], dict) 180 + if has_host and has_networks: 181 + to_return[host.id] = result_body[host.id]['networks'] 187 182 else: 188 - result_json_string = json.dumps({"error_message": "invalid response, missing 'ids' list"}) 189 - current_app.logger.error(f"""missing 'ids' list for list_ids operation, host {host.id}""") 183 + # result_json_string = json.dumps({"error_message": "invalid response, missing 'networks' list"}) 184 + current_app.logger.error(f"""missing 'networks' list for get_all_by_host_and_network operation, host {host.id}""") 190 185 except: 191 186 # no need to do anything here since if it cant be parsed then generic_operation will handle it. 192 187 pass
+7
capsulflask/schema_migrations/21_down_desired_state.sql
··· 1 + 2 + 3 + 4 + ALTER TABLE vms DROP COLUMN desired_state; 5 + 6 + UPDATE schemaversion SET version = 20; 7 +
+7
capsulflask/schema_migrations/21_up_desired_state.sql
··· 1 + 2 + 3 + 4 + ALTER TABLE vms ADD COLUMN desired_state TEXT DEFAULT 'running'; 5 + 6 + UPDATE schemaversion SET version = 21; 7 +
+1 -1
capsulflask/shared.py
··· 31 31 def get(self, id: str) -> VirtualMachine: 32 32 pass 33 33 34 - def list_ids(self) -> list: 34 + def get_all_by_host_and_network(self) -> dict: 35 35 pass 36 36 37 37 def create(self, email: str, id: str, template_image_file_name: str, vcpus: int, memory: int, ssh_authorized_keys: list):
-3
capsulflask/shell_scripts/list-ids.sh
··· 1 - #!/bin/sh 2 - 3 - virsh list --all | grep running | grep -v ' Id' | grep -v -- '----' | awk '{print $2}' | sort
+7 -7
capsulflask/shell_scripts/ssh-keyscan.sh
··· 8 8 fi 9 9 10 10 printf '[' 11 - DELIMITER="" 11 + delimiter="" 12 12 ssh-keyscan "$ip_address" 2>/dev/null | while read -r line; do 13 13 if echo "$line" | grep -qE "^$ip_address"' +(ssh|ecdsa)-[0-9A-Za-z+/_=@. -]+$'; then 14 - KEY_CONTENT="$(echo "$line" | awk '{ print $2 " " $3 }')" 15 - FINGERPRINT_OUTPUT="$(echo "$KEY_CONTENT" | ssh-keygen -l -E sha256 -f - | sed -E 's/^[0-9]+ SHA256:([0-9A-Za-z+/-]+) .+ \(([A-Z0-9]+)\)$/\1 \2/g')" 16 - SHA256_HASH="$(echo "$FINGERPRINT_OUTPUT" | awk '{ print $1 }')" 17 - KEY_TYPE="$(echo "$FINGERPRINT_OUTPUT" | awk '{ print $2 }')" 18 - printf '%s\n {"key_type":"%s", "content":"%s", "sha256":"%s"}' "$DELIMITER" "$KEY_TYPE" "$KEY_CONTENT" "$SHA256_HASH" 19 - DELIMITER="," 14 + key_content="$(echo "$line" | awk '{ print $2 " " $3 }')" 15 + fingerprint_output="$(echo "$key_content" | ssh-keygen -l -E sha256 -f - | sed -E 's/^[0-9]+ SHA256:([0-9A-Za-z+/-]+) .+ \(([A-Z0-9]+)\)$/\1 \2/g')" 16 + sha256_hash="$(echo "$fingerprint_output" | awk '{ print $1 }')" 17 + key_type="$(echo "$fingerprint_output" | awk '{ print $2 }')" 18 + printf '%s\n {"key_type":"%s", "content":"%s", "sha256":"%s"}' "$delimiter" "$key_type" "$key_content" "$sha256_hash" 19 + delimiter="," 20 20 fi 21 21 done 22 22 printf '\n]\n'
+13
capsulflask/shell_scripts/virsh-list.sh
··· 1 + #!/bin/sh 2 + 3 + printf '[' 4 + delimiter="" 5 + virsh list --all | while read -r line; do 6 + if echo "$line" | grep -qE '(running)|(shut off)'; then 7 + capsul_id="$(echo "$line" | awk '{ print $2 }')" 8 + capsul_state="$(echo "$line" | sed -E 's/.*((running)|(shut off))\w*/\1/')" 9 + printf '%s\n {"id":"%s", "state":"%s"}' "$delimiter" "$capsul_id" "$capsul_state" 10 + delimiter="," 11 + fi 12 + done 13 + printf '\n]\n'
+3
capsulflask/shell_scripts/virsh-net-list.sh
··· 1 + #!/bin/sh 2 + 3 + virsh net-list --all | tail -n +3 | awk '{ print $1 }'
+3 -3
capsulflask/spoke_api.py
··· 49 49 handlers = { 50 50 "capacity_avaliable": handle_capacity_avaliable, 51 51 "get": handle_get, 52 - "list_ids": handle_list_ids, 52 + "get_all_by_host_and_network": handle_get_all_by_host_and_network, 53 53 "create": handle_create, 54 54 "destroy": handle_destroy, 55 55 "vm_state_command": handle_vm_state_command, ··· 96 96 97 97 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)) 98 98 99 - def handle_list_ids(operation_id, request_body): 100 - return jsonify(dict(assignment_status="assigned", ids=current_app.config['SPOKE_MODEL'].list_ids())) 99 + def handle_get_all_by_host_and_network(operation_id, request_body): 100 + return jsonify(dict(assignment_status="assigned", ids=current_app.config['SPOKE_MODEL'].get_all_by_host_and_network())) 101 101 102 102 def handle_create(operation_id, request_body): 103 103 if not operation_id:
+6 -5
capsulflask/spoke_model.py
··· 38 38 39 39 return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4=ipv4, state="running") 40 40 41 - def list_ids(self) -> list: 42 - return get_model().all_non_deleted_vm_ids() 41 + def get_all_by_host_and_network(self) -> dict: 42 + return get_model().non_deleted_vms_by_host_and_network(None) 43 43 44 44 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): 45 45 validate_capsul_id(id) ··· 133 133 134 134 return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], state=state, ipv4=ipaddr) 135 135 136 - def list_ids(self) -> list: 137 - completedProcess = run([join(current_app.root_path, 'shell_scripts/list-ids.sh')], capture_output=True) 136 + def get_all_by_host_and_network(self) -> list: 137 + # TODO implement this 138 + completedProcess = run([join(current_app.root_path, 'shell_scripts/virsh-list.sh')], capture_output=True) 138 139 self.validate_completed_process(completedProcess) 139 - return list(map(lambda x: x.decode("utf-8"), completedProcess.stdout.splitlines() )) 140 + return json.loads(completedProcess.stdout.decode("utf-8")) 140 141 141 142 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): 142 143 validate_capsul_id(id)