+2
-2
capsulflask/__init__.py
+2
-2
capsulflask/__init__.py
···
70
70
71
71
VM_STORAGE_DIR=os.environ.get("VM_STORAGE_DIR", default="/tank/vm"),
72
72
BACKUP_STORAGE_MOUNTS=os.environ.get("BACKUP_STORAGE_MOUNTS", default="/mnt/backup1"),
73
-
BACKUP_DEBUG_LOG=os.environ.get("BACKUP_DEBUG_LOG", default="true"),
73
+
BACKUP_DEBUG_LOG=os.environ.get("BACKUP_DEBUG_LOG", default="false"),
74
74
75
75
MAIL_DELIVERY_MANAGER_URL=os.environ.get("MAIL_DELIVERY_MANAGER_URL", default=""),
76
76
MAIL_DELIVERY_MANAGER_SECRET=os.environ.get("MAIL_DELIVERY_MANAGER_SECRET", default=""),
···
221
221
222
222
backup_maintenance_task_url = f"{app.config['HUB_URL']}/hub/backup-maintenance"
223
223
trigger_backup_maintenance_task = lambda: requests.post(backup_maintenance_task_url, headers=hub_token_headers)
224
-
scheduler.add_job(name="backup-maintenance", func=trigger_backup_maintenance_task, trigger="interval", seconds=10)
224
+
scheduler.add_job(name="backup-maintenance", func=trigger_backup_maintenance_task, trigger="interval", seconds=15)
225
225
226
226
scheduler.start()
227
227
+40
-7
capsulflask/console.py
+40
-7
capsulflask/console.py
···
17
17
from capsulflask.metrics import metric_durations
18
18
from capsulflask.auth import account_required
19
19
from capsulflask.db import get_model
20
-
from capsulflask.shared import my_exec_info_message, get_warnings_list, get_warning_headline, get_vm_months_float, get_account_balance, average_number_of_days_in_a_month
20
+
from capsulflask.shared import my_exec_info_message, get_warnings_list, get_warning_headline, get_vm_months_float, get_account_balance, average_number_of_days_in_a_month, VirtualMachine
21
21
from capsulflask.payment import poll_btcpay_session, check_if_shortterm_flag_can_be_unset
22
22
from capsulflask import cli
23
23
···
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, existing_ipv4, get_ssh_host_keys):
30
+
def double_check_capsul_address(id, existing_ipv4, get_ssh_host_keys, get_agent_details) -> VirtualMachine:
31
31
try:
32
-
result = current_app.config["HUB_MODEL"].get(id, get_ssh_host_keys)
32
+
result = current_app.config["HUB_MODEL"].get(id, get_ssh_host_keys, get_agent_details)
33
33
34
34
if result != None and result.ipv4 != "" and (existing_ipv4 == None or existing_ipv4 == ""):
35
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)
39
+
40
+
if result != None and get_agent_details:
41
+
get_model().update_vm_agent_details(email=session["account"], id=id, agent_socket=result.agent_socket, guest_agent=result.guest_agent, acpid=result.acpid)
39
42
except:
40
43
current_app.logger.error(f"""
41
44
the virtualization model threw an error in double_check_capsul_address of {id}:
···
59
62
# for now we are going to check the IP according to the virt model
60
63
# on every request. this could be done by a background job and cached later on...
61
64
for vm in vms:
62
-
result = double_check_capsul_address(vm["id"], vm["ipv4"], False)
65
+
result = double_check_capsul_address(vm["id"], vm["ipv4"], False, False)
63
66
if result is not None:
64
67
vm["ipv4"] = result.ipv4
65
68
vm["state"] = result.state
···
145
148
check_if_shortterm_flag_can_be_unset(session['account'])
146
149
147
150
return render_template("capsul-detail.html", vm=vm, deleted=True)
151
+
elif request.form['action'] == "acpi-shutdown":
152
+
current_app.logger.info(f"acpi-shutdown {vm['id']} per user request ({session['account']})")
153
+
get_model().set_desired_state(email=session["account"], vm_id=id, desired_state="shut off")
154
+
current_app.config["HUB_MODEL"].vm_state_command(email=session['account'], id=id, command="acpi-shutdown")
155
+
vm["state"] = "stopping"
156
+
flash("please note that your account will still be billed for this capsul while it is in a stopped state")
157
+
158
+
return render_template(
159
+
"capsul-detail.html",
160
+
csrf_token = session["csrf-token"],
161
+
vm=vm,
162
+
durations=list(map(lambda x: x.strip("_"), metric_durations.keys())),
163
+
duration=duration
164
+
)
165
+
148
166
elif request.form['action'] == "force-stop":
149
167
if 'are_you_sure' not in request.form or not request.form['are_you_sure']:
150
168
return render_template(
···
174
192
175
193
176
194
else:
177
-
needs_ssh_host_keys = "ssh_host_keys" not in vm or len(vm["ssh_host_keys"]) == 0
195
+
get_ssh_host_keys = "ssh_host_keys" not in vm or len(vm["ssh_host_keys"]) == 0
196
+
get_agent_details = True
178
197
179
-
vm_from_virt_model = double_check_capsul_address(vm["id"], vm["ipv4"], needs_ssh_host_keys)
198
+
vm_from_virt_model = double_check_capsul_address(vm["id"], vm["ipv4"], get_ssh_host_keys, get_agent_details)
180
199
181
200
if vm_from_virt_model is not None:
182
201
vm["ipv4"] = vm_from_virt_model.ipv4
183
202
vm["state"] = vm_from_virt_model.state
184
-
if needs_ssh_host_keys:
203
+
if get_ssh_host_keys:
185
204
vm["ssh_host_keys"] = vm_from_virt_model.ssh_host_keys
205
+
if get_agent_details:
206
+
vm["agent_socket"] = vm_from_virt_model.agent_socket
207
+
vm["guest_agent"] = vm_from_virt_model.guest_agent
208
+
vm["acpid"] = vm_from_virt_model.acpid
186
209
else:
187
210
vm["state"] = "unknown"
188
211
189
212
if vm["state"] == "running" and not vm["ipv4"]:
190
213
vm["state"] = "starting"
214
+
215
+
vm['last_backup_str'] = 'never'
216
+
if vm['last_backup_finished']:
217
+
seconds_since_last_backup_finished = (datetime.utcnow() - vm['last_backup_finished']).total_seconds()
218
+
if seconds_since_last_backup_finished < 60*60:
219
+
vm['last_backup_str'] = f"{round(seconds_since_last_backup_finished/60)} minutes ago"
220
+
elif seconds_since_last_backup_finished < 60*60*24:
221
+
vm['last_backup_str'] = f"{round(seconds_since_last_backup_finished/(60*60))} hours ago"
222
+
elif seconds_since_last_backup_finished < 60*60*24*365:
223
+
vm['last_backup_str'] = f"{round(seconds_since_last_backup_finished/(60*60*24))} days ago"
191
224
192
225
return render_template(
193
226
"capsul-detail.html",
+7
-2
capsulflask/db_model.py
+7
-2
capsulflask/db_model.py
···
164
164
)
165
165
self.connection.commit()
166
166
167
+
def update_vm_agent_details(self, email, id, agent_socket, guest_agent, acpid):
168
+
self.cursor.execute("UPDATE vms SET has_agent_socket = %s, has_guest_agent = %s, has_acpid = %s WHERE email = %s AND id = %s", (agent_socket, guest_agent, acpid, email, id))
169
+
self.connection.commit()
170
+
167
171
def create_vm(self, email, id, size, shortterm, os, host, network_name, public_ipv4, ssh_authorized_keys):
168
172
self.cursor.execute("""
169
173
INSERT INTO vms (email, id, size, shortterm, os, host, network_name, public_ipv4)
···
188
192
def get_vm_detail(self, email, id):
189
193
self.cursor.execute("""
190
194
SELECT vms.id, vms.public_ipv4, vms.public_ipv6, os_images.description, vms.created, vms.deleted,
191
-
vm_sizes.id, vms.shortterm, vm_sizes.dollars_per_month, vm_sizes.vcpus, vm_sizes.memory_mb, vm_sizes.bandwidth_gb_per_month
195
+
vm_sizes.id, vms.shortterm, vm_sizes.dollars_per_month, vm_sizes.vcpus, vm_sizes.memory_mb, vm_sizes.bandwidth_gb_per_month,
196
+
vms.has_agent_socket, vms.has_guest_agent, vms.has_acpid, vms.last_backup_finished
192
197
FROM vms
193
198
JOIN os_images on vms.os = os_images.id
194
199
JOIN vm_sizes on vms.size = vm_sizes.id
···
202
207
vm = dict(
203
208
id=row[0], ipv4=row[1], ipv6=row[2], os_description=row[3], created=row[4], deleted=row[5],
204
209
size=row[6], shortterm=row[7], dollars_per_month=row[8], vcpus=row[9], memory_mb=row[10],
205
-
bandwidth_gb_per_month=row[11],
210
+
bandwidth_gb_per_month=row[11], agent_socket=row[12], guest_agent=row[13], acpid=row[14], last_backup_finished=row[15]
206
211
)
207
212
208
213
self.cursor.execute("""
+6
-6
capsulflask/hub_model.py
+6
-6
capsulflask/hub_model.py
···
24
24
def capacity_avaliable(self, additional_ram_bytes):
25
25
return True
26
26
27
-
def get(self, id, get_ssh_host_keys):
27
+
def get(self, id, get_ssh_host_keys: bool, get_agent_details: bool):
28
28
validate_capsul_id(id)
29
29
30
+
ssh_host_keys = []
30
31
if get_ssh_host_keys:
31
32
ssh_host_keys = json.loads("""[
32
33
{"key_type":"ED25519", "content":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN8cna0zeKSKl/r8whdn/KmDWhdzuWRVV0GaKIM+eshh", "sha256":"V4X2apAF6btGAfS45gmpldknoDX0ipJ5c6DLfZR2ttQ"},
33
34
{"key_type":"RSA", "content":"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvotgzgEP65JUQ8S8OoNKy1uEEPEAcFetSp7QpONe6hj4wPgyFNgVtdoWdNcU19dX3hpdse0G8OlaMUTnNVuRlbIZXuifXQ2jTtCFUA2mmJ5bF+XjGm3TXKMNGh9PN+wEPUeWd14vZL+QPUMev5LmA8cawPiU5+vVMLid93HRBj118aCJFQxLgrdP48VPfKHFRfCR6TIjg1ii3dH4acdJAvlmJ3GFB6ICT42EmBqskz2MPe0rIFxH8YohCBbAbrbWYcptHt4e48h4UdpZdYOhEdv89GrT8BF2C5cbQ5i9qVpI57bXKrj8hPZU5of48UHLSpXG8mbH0YDiOQOfKX/Mt", "sha256":"ghee6KzRnBJhND2kEUZSaouk7CD6o6z2aAc8GPkV+GQ"},
34
35
{"key_type":"ECDSA", "content":"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLLgOoATz9R4aS2kk7vWoxX+lshK63t9+5BIHdzZeFE1o+shlcf0Wji8cN/L1+m3bi0uSETZDOAWMP3rHLJj9Hk=", "sha256":"aCYG1aD8cv/TjzJL0bi9jdabMGksdkfa7R8dCGm1yYs"}
35
36
]""")
36
-
return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4=self.default_ipv4, ssh_host_keys=ssh_host_keys)
37
-
38
-
return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4=self.default_ipv4)
37
+
38
+
return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4=self.default_ipv4, state="running", ssh_host_keys=ssh_host_keys, guest_agent=True, agent_socket=True, acpid=True)
39
39
40
40
def get_all_by_id(self) -> dict:
41
41
by_host_and_network = get_model().non_deleted_vms_by_host_and_network(None)
···
166
166
return False
167
167
168
168
169
-
def get(self, id, get_ssh_host_keys) -> VirtualMachine:
169
+
def get(self, id, get_ssh_host_keys, get_agent_details) -> VirtualMachine:
170
170
validate_capsul_id(id)
171
171
host = get_model().host_of_capsul(id)
172
172
if host is not None:
173
-
payload = json.dumps(dict(type="get", id=id, get_ssh_host_keys=get_ssh_host_keys))
173
+
payload = json.dumps(dict(type="get", id=id, get_ssh_host_keys=get_ssh_host_keys, get_agent_details=get_agent_details))
174
174
results = self.synchronous_operation([host], None, payload)
175
175
for result in results:
176
176
try:
+2
capsulflask/schema_migrations/27_up_backups.sql
+2
capsulflask/schema_migrations/27_up_backups.sql
···
1
+
ALTER TABLE vms ADD COLUMN has_agent_socket BOOLEAN NOT NULL DEFAULT FALSE;
1
2
ALTER TABLE vms ADD COLUMN has_guest_agent BOOLEAN NOT NULL DEFAULT FALSE;
3
+
ALTER TABLE vms ADD COLUMN has_acpid BOOLEAN NOT NULL DEFAULT FALSE;
2
4
ALTER TABLE vms ADD COLUMN last_backup_started TIMESTAMP;
3
5
ALTER TABLE vms ADD COLUMN last_backup_finished TIMESTAMP;
4
6
+36
capsulflask/shell_scripts/acpi-shutdown.sh
+36
capsulflask/shell_scripts/acpi-shutdown.sh
···
1
+
#!/bin/sh -e
2
+
3
+
# State Key
4
+
# NOSTATE: 0 - no state
5
+
# RUNNING: 1 - the domain is running
6
+
# BLOCKED: 2 - the domain is blocked on resource
7
+
# PAUSED: 3 - the domain is paused by user
8
+
# SHUTDOWN: 4 - the domain is being shut down
9
+
# SHUTOFF: 5 - the domain is shut off
10
+
# CRASHED: 6 - the domain is crashed
11
+
# PMSUSPENDED: 7 - the domain is suspended by guest power management
12
+
# LAST: 8 NB: this enum value will increase over time as new events are added to the libvirt API. It reflects the last state supported by this version of the libvirt API.
13
+
14
+
vmname="$1"
15
+
16
+
[ "$vmname" = '' ] && printf 'you must set $vmname\n' && exit 1
17
+
18
+
if printf "$vmname" | grep -vqE '^(cvm|capsul)-[a-z0-9]{10}$'; then
19
+
printf 'vmname %s must match ^capsul-[a-z0-9]{10}$\n' "$vmname"
20
+
exit 1
21
+
fi
22
+
23
+
state="$(virsh domstats $vmname | grep state.state | cut -d '=' -f 2)"
24
+
25
+
[ "$state" = '' ] && printf 'state was not detected. must match ^[0-8]$\n' && exit 1
26
+
27
+
if printf "$state" | grep -vqE '^[0-8]$'; then
28
+
printf 'state %s must match ^[0-8]$\n' "$state"
29
+
exit 1
30
+
fi
31
+
32
+
case "$state" in
33
+
[1-47]) virsh shutdown --mode acpi "$vmname" > /dev/null ;;
34
+
[5-6]) printf "%s is already off\n" "$vmname" ;;
35
+
esac
36
+
+44
capsulflask/shell_scripts/get-guest-agent.sh
+44
capsulflask/shell_scripts/get-guest-agent.sh
···
1
+
#!/bin/sh -e
2
+
3
+
vmname="$1"
4
+
5
+
if echo "$vmname" | grep -vqE '^(cvm|capsul)-[a-z0-9]{10}$'; then
6
+
echo "vmname $vmname must match "'"^capsul-[a-z0-9]{10}$"'
7
+
exit 1
8
+
fi
9
+
10
+
qemuAgentCommandOutput="no-guest-agent-socket"
11
+
if virsh dumpxml "$vmname" | grep -q guest_agent ; then
12
+
isAcpidRunning='{"execute": "guest-exec", "arguments":{"path":"/usr/bin/env", "arg":["pidof","acpid"], "capture-output":true}}'
13
+
qemuAgentCommandOutput="$( { virsh qemu-agent-command "$vmname" --cmd "$isAcpidRunning" || printf '%s' 'no-qemu-guest-agent'; } | tr -d '\n' )"
14
+
15
+
if [ "$qemuAgentCommandOutput" != "no-qemu-guest-agent" ] ; then
16
+
# {"return":{"pid":2967}}
17
+
pidInsideVm="$(echo "$qemuAgentCommandOutput" | jq -r '.return.pid')"
18
+
qemuAgentCommandOutput="error"
19
+
if echo "$pidInsideVm" | grep -vqE '^[0-9]+$'; then
20
+
echo "pidInsideVm: $pidInsideVm WTF!!?!?"
21
+
exit 1
22
+
fi
23
+
getStatusOfProcessInsideVm='{"execute": "guest-exec-status", "arguments":{"pid":'"$pidInsideVm"'}}'
24
+
exited='false'
25
+
tries=0
26
+
while [ "$exited" = "false" ] && [ "$tries" -lt 10 ] ; do
27
+
tries=$((tries+1))
28
+
sleep 0.1
29
+
processStatusInfo="$( virsh qemu-agent-command "$vmname" --cmd "$getStatusOfProcessInsideVm" )"
30
+
# {"return":{"exitcode":0,"out-data":"MTgwNAo=","exited":true}}
31
+
exited="$(echo "$processStatusInfo" | jq -r '.return.exited')"
32
+
if [ "$exited" = "true" ] ; then
33
+
exitCode="$(echo "$processStatusInfo" | jq -r '.return.exitcode')"
34
+
if [ "$exitCode" = "0" ] ; then
35
+
qemuAgentCommandOutput="ok"
36
+
else
37
+
qemuAgentCommandOutput="no-acpid"
38
+
fi
39
+
fi
40
+
done
41
+
fi
42
+
fi
43
+
44
+
echo "$qemuAgentCommandOutput"
+1
-1
capsulflask/shell_scripts/get.sh
+1
-1
capsulflask/shell_scripts/get.sh
+2
-2
capsulflask/spoke_api.py
+2
-2
capsulflask/spoke_api.py
···
91
91
if 'id' not in request_body:
92
92
current_app.logger.error(f"/hosts/operation returned 400: id is required for get")
93
93
return abort(400, f"bad request; id is required for get")
94
-
95
-
vm = current_app.config['SPOKE_MODEL'].get(request_body['id'], request_body['get_ssh_host_keys'])
94
+
95
+
vm = current_app.config['SPOKE_MODEL'].get(request_body['id'], request_body['get_ssh_host_keys'], request_body['get_agent_details'])
96
96
if vm is None:
97
97
return jsonify(dict(assignment_status="assigned"))
98
98
+29
-7
capsulflask/spoke_model.py
+29
-7
capsulflask/spoke_model.py
···
27
27
def capacity_avaliable(self, additional_ram_bytes):
28
28
return True
29
29
30
-
def get(self, id, get_ssh_host_keys):
30
+
def get(self, id, get_ssh_host_keys: bool, get_agent_details: bool):
31
31
validate_capsul_id(id)
32
32
33
33
ipv4 = "1.1.1.1"
34
34
if id in self.capsuls:
35
35
ipv4 = self.capsuls[id]['public_ipv4']
36
36
37
+
ssh_host_keys=[]
37
38
if get_ssh_host_keys:
38
39
ssh_host_keys = json.loads("""[
39
40
{"key_type":"ED25519", "content":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN8cna0zeKSKl/r8whdn/KmDWhdzuWRVV0GaKIM+eshh", "sha256":"V4X2apAF6btGAfS45gmpldknoDX0ipJ5c6DLfZR2ttQ"},
40
41
{"key_type":"RSA", "content":"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvotgzgEP65JUQ8S8OoNKy1uEEPEAcFetSp7QpONe6hj4wPgyFNgVtdoWdNcU19dX3hpdse0G8OlaMUTnNVuRlbIZXuifXQ2jTtCFUA2mmJ5bF+XjGm3TXKMNGh9PN+wEPUeWd14vZL+QPUMev5LmA8cawPiU5+vVMLid93HRBj118aCJFQxLgrdP48VPfKHFRfCR6TIjg1ii3dH4acdJAvlmJ3GFB6ICT42EmBqskz2MPe0rIFxH8YohCBbAbrbWYcptHt4e48h4UdpZdYOhEdv89GrT8BF2C5cbQ5i9qVpI57bXKrj8hPZU5of48UHLSpXG8mbH0YDiOQOfKX/Mt", "sha256":"ghee6KzRnBJhND2kEUZSaouk7CD6o6z2aAc8GPkV+GQ"},
41
42
{"key_type":"ECDSA", "content":"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLLgOoATz9R4aS2kk7vWoxX+lshK63t9+5BIHdzZeFE1o+shlcf0Wji8cN/L1+m3bi0uSETZDOAWMP3rHLJj9Hk=", "sha256":"aCYG1aD8cv/TjzJL0bi9jdabMGksdkfa7R8dCGm1yYs"}
42
43
]""")
43
-
return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4=ipv4, state="running", ssh_host_keys=ssh_host_keys)
44
44
45
-
return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4=ipv4, state="running")
45
+
return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4=ipv4, state="running", ssh_host_keys=ssh_host_keys, guest_agent=True, agent_socket=True, acpid=True)
46
46
47
47
def get_all_by_id(self) -> dict:
48
48
by_host_and_network = get_model().non_deleted_vms_by_host_and_network(current_app.config["SPOKE_HOST_ID"])
···
114
114
115
115
return True
116
116
117
-
def get(self, id, get_ssh_host_keys):
117
+
def get(self, id, get_ssh_host_keys: bool, get_agent_details: bool):
118
118
validate_capsul_id(id)
119
119
completed_process = run([join(current_app.root_path, 'shell_scripts/get.sh'), id], capture_output=True)
120
120
self.validate_completed_process(completed_process)
···
143
143
if not re.match(r"^([0-9]{1,3}\.){3}[0-9]{1,3}$", ipaddr):
144
144
return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], state=state)
145
145
146
+
guest_agent=False
147
+
agent_socket=False
148
+
acpid=False
149
+
ssh_host_keys = []
146
150
if get_ssh_host_keys:
147
151
try:
148
152
completed_process2 = run([join(current_app.root_path, 'shell_scripts/ssh-keyscan.sh'), ipaddr], capture_output=True)
149
153
self.validate_completed_process(completed_process2)
150
154
ssh_host_keys = json.loads(completed_process2.stdout.decode("utf-8"))
151
-
return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], state=state, ipv4=ipaddr, ssh_host_keys=ssh_host_keys)
152
155
except:
153
156
current_app.logger.warning(f"""
154
157
failed to ssh-keyscan {id} at {ipaddr}:
155
158
{my_exec_info_message(sys.exc_info())}"""
156
159
)
157
160
158
-
return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], state=state, ipv4=ipaddr)
161
+
if get_agent_details:
162
+
try:
163
+
completed_process3 = run([join(current_app.root_path, 'shell_scripts/get-guest-agent.sh'), id], capture_output=True)
164
+
self.validate_completed_process(completed_process3)
165
+
result = completed_process3.stdout.decode("utf-8")
166
+
if result != 'no-guest-agent-socket':
167
+
agent_socket = True
168
+
if result == "ok" or result == 'no-acpid':
169
+
guest_agent = True
170
+
if result == "ok":
171
+
acpid = True
172
+
if result not in ['ok', 'no-acpid', 'no-qemu-guest-agent', 'no-guest-agent-socket']:
173
+
raise ValueError(result)
174
+
except:
175
+
current_app.logger.warning(f"""
176
+
failed to get-guest-agent {id} at {ipaddr}:
177
+
{my_exec_info_message(sys.exc_info())}"""
178
+
)
179
+
180
+
return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], state=state, ipv4=ipaddr, ssh_host_keys=ssh_host_keys, agent_socket=agent_socket, guest_agent=guest_agent, acpid=acpid)
159
181
160
182
def get_all_by_id(self) -> dict:
161
183
···
300
322
301
323
def vm_state_command(self, email: str, id: str, command: str):
302
324
validate_capsul_id(id)
303
-
if command not in ["stop", "force-stop", "start", "restart"]:
325
+
if command not in ["stop", "force-stop", "acpi-shutdown", "start", "restart"]:
304
326
raise ValueError(f"command ({command}) must be one of stop, force-stop, start, or restart")
305
327
306
328
completed_process = run([join(current_app.root_path, f"shell_scripts/{command}.sh"), id], capture_output=True)
+12
-4
capsulflask/templates/capsul-detail.html
+12
-4
capsulflask/templates/capsul-detail.html
···
108
108
<label class="align" for="ssh_username">SSH Username</label>
109
109
<span id="ssh_username">cyberian</span>
110
110
</div>
111
-
112
111
<div class="row justify-start">
113
-
<!-- spacer to make authorized keys show up on its own line -->
114
-
<label class="align" for="spacer"> </label>
115
-
<span id="spacer"> </span>
112
+
<label class="align" for="last_backup">Last Backup</label>
113
+
<span id="last_backup">{{ vm['last_backup_str'] }}</span>
116
114
</div>
117
115
<div class="row justify-start">
118
116
<label class="align" for="ssh_authorized_keys">SSH Authorized Keys</label>
···
121
119
</div>
122
120
123
121
</div>
122
+
<div class="row">
123
+
<hr/>
124
+
</div>
124
125
<div class="row center justify-start vm-actions">
125
126
<label class="align" for="delete_action">Actions</label>
126
127
<form id="delete_action" method="post">
···
136
137
</form>
137
138
{% endif %}
138
139
{% if vm['state'] != 'stopped' %}
140
+
{% if vm['acpid'] %}
141
+
<form id="acpi_shutdown_action" method="post">
142
+
<input type="hidden" name="action" value="acpi-shutdown"/>
143
+
<input type="hidden" name="csrf-token" value="{{ csrf_token }}"/>
144
+
<input type="submit" class="form-submit-link" value="[ ACPI Shutdown... ]">
145
+
</form>
146
+
{% endif %}
139
147
<form id="force_stop_action" method="post">
140
148
<input type="hidden" name="action" value="force-stop"/>
141
149
<input type="hidden" name="csrf-token" value="{{ csrf_token }}"/>