+34
capsulflask/admin.py
+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
+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
+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
+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
+1
-1
capsulflask/db.py
+9
-8
capsulflask/db_model.py
+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
+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
+7
capsulflask/schema_migrations/21_down_desired_state.sql
+7
capsulflask/schema_migrations/21_up_desired_state.sql
+7
capsulflask/schema_migrations/21_up_desired_state.sql
-3
capsulflask/shell_scripts/list-ids.sh
-3
capsulflask/shell_scripts/list-ids.sh
+7
-7
capsulflask/shell_scripts/ssh-keyscan.sh
+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
+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
+3
capsulflask/shell_scripts/virsh-net-list.sh
+3
-3
capsulflask/spoke_api.py
+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
+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)