Skip to content

Commit 85101ac

Browse files
committed
Error handling and custom actions for hyperv
1 parent cb38175 commit 85101ac

File tree

2 files changed

+201
-84
lines changed

2 files changed

+201
-84
lines changed

engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -406,9 +406,9 @@ BEGIN
406406
; END IF
407407
;END;
408408

409-
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('Proxmox', 'CreateSnapshot', 'Create Instance Snapshot', 'VirtualMachine', 15, 'Snapshot created for {{resourceName}} in {{extensionName}}', 'Snapshot creation failed for {{resourceName}}', 60);
410-
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('Proxmox', 'RestoreSnapshot', 'Restore Instance Snapshot', 'VirtualMachine', 15, 'Successfully restored snapshot for {{resourceName}} in {{extensionName}}', 'Restore snapshot failed for {{resourceName}}', 60);
411-
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('Proxmox', 'DeleteSnapshot', 'Delete Instance Snapshot', 'VirtualMachine', 15, 'Successfully deleted snapshot for {{resourceName}} in {{extensionName}}', 'Delete snapshot failed for {{resourceName}}', 60);
409+
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('Proxmox', 'CreateSnapshot', 'Create an Instance snapshot', 'VirtualMachine', 15, 'Snapshot created for {{resourceName}} in {{extensionName}}', 'Snapshot creation failed for {{resourceName}}', 60);
410+
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('Proxmox', 'RestoreSnapshot', 'Restore Instance to the specifeid snapshot', 'VirtualMachine', 15, 'Successfully restored snapshot for {{resourceName}} in {{extensionName}}', 'Restore snapshot failed for {{resourceName}}', 60);
411+
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('Proxmox', 'DeleteSnapshot', 'Delete the specified snapshot', 'VirtualMachine', 15, 'Successfully deleted snapshot for {{resourceName}} in {{extensionName}}', 'Delete snapshot failed for {{resourceName}}', 60);
412412

413413
CALL cloud.INSERT_EXTENSION_CUSTOM_ACTION_DETAILS_IF_NOT_EXISTS(
414414
'Proxmox',
@@ -458,3 +458,56 @@ CALL cloud.INSERT_EXTENSION_CUSTOM_ACTION_DETAILS_IF_NOT_EXISTS(
458458
}
459459
]'
460460
);
461+
462+
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('HyperV', 'CreateSnapshot', 'Create a checkpoint/snapshot for the Instance', 'VirtualMachine', 15, 'Snapshot created for {{resourceName}} in {{extensionName}}', 'Snapshot creation failed for {{resourceName}}', 60);
463+
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('HyperV', 'RestoreSnapshot', 'Restore Instance to the specified snapshot', 'VirtualMachine', 15, 'Successfully restored snapshot for {{resourceName}} in {{extensionName}}', 'Restore snapshot failed for {{resourceName}}', 60);
464+
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('HyperV', 'DeleteSnapshot', 'Delete the specified snapshot', 'VirtualMachine', 15, 'Successfully deleted snapshot for {{resourceName}} in {{extensionName}}', 'Delete snapshot failed for {{resourceName}}', 60);
465+
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('HyperV', 'Suspend', 'Suspend the Instance by freezing its current state in RAM', 'VirtualMachine', 15, 'Successfully suspended {{resourceName}} in {{extensionName}}', 'Suspend failed for {{resourceName}}', 60);
466+
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('HyperV', 'Resume', 'Resumes a suspended Instance, restoring CPU execution from memory.', 'VirtualMachine', 15, 'Successfully resumed {{resourceName}} in {{extensionName}}', 'Resume failed for {{resourceName}}', 60);
467+
468+
CALL cloud.INSERT_EXTENSION_CUSTOM_ACTION_DETAILS_IF_NOT_EXISTS(
469+
'HyperV',
470+
'CreateSnapshot',
471+
'[
472+
{
473+
"name": "snapshot_name",
474+
"type": "STRING",
475+
"validationformat": "NONE",
476+
"required": true
477+
}
478+
]'
479+
);
480+
CALL cloud.INSERT_EXTENSION_CUSTOM_ACTION_DETAILS_IF_NOT_EXISTS(
481+
'HyperV',
482+
'RestoreSnapshot',
483+
'[
484+
{
485+
"name": "snapshot_name",
486+
"type": "STRING",
487+
"validationformat": "NONE",
488+
"required": true
489+
}
490+
]'
491+
);
492+
CALL cloud.INSERT_EXTENSION_CUSTOM_ACTION_DETAILS_IF_NOT_EXISTS(
493+
'HyperV',
494+
'DeleteSnapshot',
495+
'[
496+
{
497+
"name": "snapshot_name",
498+
"type": "STRING",
499+
"validationformat": "NONE",
500+
"required": true
501+
}
502+
]'
503+
);
504+
CALL cloud.INSERT_EXTENSION_CUSTOM_ACTION_DETAILS_IF_NOT_EXISTS(
505+
'HyperV',
506+
'Suspend',
507+
'[]'
508+
);
509+
CALL cloud.INSERT_EXTENSION_CUSTOM_ACTION_DETAILS_IF_NOT_EXISTS(
510+
'HyperV',
511+
'Resume',
512+
'[]'
513+
);

extensions/HyperV/hyperv.py

Lines changed: 145 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -31,25 +31,28 @@ def succeed(data):
3131
sys.exit(0)
3232

3333

34-
def run_powershell_ssh(command, url, username, password):
35-
try:
36-
print(f"[INFO] Connecting to {url} as {username}...")
37-
ssh = paramiko.SSHClient()
38-
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
39-
ssh.connect(url, username=username, password=password)
34+
def run_powershell_ssh_int(command, url, username, password):
35+
#print(f"[INFO] Connecting to {url} as {username}...")
36+
ssh = paramiko.SSHClient()
37+
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
38+
ssh.connect(url, username=username, password=password)
4039

41-
ps_command = f'powershell -NoProfile -Command "{command.strip()}"'
42-
print(f"[INFO] Executing: {ps_command}")
43-
stdin, stdout, stderr = ssh.exec_command(ps_command)
40+
ps_command = f'powershell -NoProfile -Command "{command.strip()}"'
41+
#print(f"[INFO] Executing: {ps_command}")
42+
stdin, stdout, stderr = ssh.exec_command(ps_command)
4443

45-
output = stdout.read().decode().strip()
46-
error = stderr.read().decode().strip()
47-
ssh.close()
44+
output = stdout.read().decode().strip()
45+
error = stderr.read().decode().strip()
46+
ssh.close()
4847

49-
if error:
50-
fail(error)
51-
else:
52-
return output
48+
if error:
49+
raise Exception(error)
50+
return output
51+
52+
def run_powershell_ssh(command, url, username, password):
53+
try:
54+
output = run_powershell_ssh_int(command, url, username, password)
55+
return output
5356
except Exception as e:
5457
fail(str(e))
5558

@@ -79,49 +82,65 @@ def create(data):
7982
memory = data["memory"]
8083
memory_mb = int(memory) / 1024 / 1024
8184
template_path = data["template_path"]
82-
#template_path = data.get("template_path", f"C:\\ProgramData\\Microsoft\\Windows\\Virtual Hard Disks\\m12-template.vhdx")
8385
vhd_path = data["default_vhd_path"] + "\\" + vm_name + ".vhdx"
8486
vhd_size_gb = data["vhd_size_gb"]
8587
generation = data["generation"]
8688
iso_path = data["iso_path"]
87-
#iso_path = data.get("iso_path", "C:\\Users\\Abhisar\\Downloads\\ubuntu-25.04-live-server-amd64.iso")
88-
switch_name = data.get["switch_name"]
89-
vm_path = data.get["vm_path"]
89+
switch_name = data["switch_name"]
90+
vm_path = data["default_vm_path"]
9091
template_type = data.get("template_type", "template")
9192

92-
command = (
93-
f'New-VM -Name \\"{vm_name}\\" -MemoryStartupBytes {memory_mb}MB '
94-
f'-Generation {generation} -Path \\"{vm_path}\\" '
95-
)
96-
if template_type == "iso":
97-
if (iso_path == ""):
98-
fail("ISO path is required")
99-
command += (
100-
f'-NewVHDPath \\"{vhd_path}\\" -NewVHDSizeBytes {vhd_size_gb}GB; '
101-
f'Add-VMDvdDrive -VMName \\"{vm_name}\\" -Path \\"{iso_path}\\"; '
102-
)
103-
else:
104-
if (template_path == ""):
105-
fail("Template path is required")
106-
run_powershell_ssh(f'Copy-Item \\"{template_path}\\" \\"{vhd_path}\\"', data["url"], data["username"], data["password"])
107-
command += f'-VHDPath \\"{vhd_path}\\"; '
108-
109-
command += (
110-
f'Set-VMProcessor -VMName \\"{vm_name}\\" -Count \\"{cpus}\\"; '
111-
f'Connect-VMNetworkAdapter -VMName \\"{vm_name}\\" -SwitchName \\"{switch_name}\\"; '
112-
f'Set-VMNetworkAdapter -VMName "{vm_name}" -StaticMacAddress "{data["mac_address"]}"; '
113-
f'Set-VMFirmware -VMName "{vm_name}" -EnableSecureBoot Off; '
114-
)
115-
116-
run_powershell_ssh(command, data["url"], data["username"], data["password"])
93+
if (data["mac_address"] == ""):
94+
fail("Mac address not found")
11795

118-
# Switch should have vlan support (External/Internal)
119-
# run_powershell_ssh(f'Set-VMNetworkAdapterVlan -VMName "{"vm_name"}" -Access -VlanId "{data["cloudstack.vlan"]}"', data["url"], data["username"], data["password"])
96+
vhd_created = False
97+
vm_created = False
98+
vm_started = False
99+
try:
100+
command = (
101+
f'New-VM -Name \\"{vm_name}\\" -MemoryStartupBytes {memory_mb}MB '
102+
f'-Generation {generation} -Path \\"{vm_path}\\" '
103+
)
104+
if template_type == "iso":
105+
if (iso_path == ""):
106+
fail("ISO path is required")
107+
command += (
108+
f'-NewVHDPath \\"{vhd_path}\\" -NewVHDSizeBytes {vhd_size_gb}GB; '
109+
f'Add-VMDvdDrive -VMName \\"{vm_name}\\" -Path \\"{iso_path}\\"; '
110+
)
111+
else:
112+
if (template_path == ""):
113+
fail("Template path is required")
114+
run_powershell_ssh_int(f'Copy-Item \\"{template_path}\\" \\"{vhd_path}\\"', data["url"], data["username"], data["password"])
115+
vhd_created = True
116+
command += f'-VHDPath \\"{vhd_path}\\"; '
117+
118+
run_powershell_ssh_int(command, data["url"], data["username"], data["password"])
119+
vm_created = True
120+
121+
command = (
122+
f'Set-VMProcessor -VMName \\"{vm_name}\\" -Count \\"{cpus}\\"; '
123+
f'Connect-VMNetworkAdapter -VMName \\"{vm_name}\\" -SwitchName \\"{switch_name}\\"; '
124+
f'Set-VMNetworkAdapter -VMName "{vm_name}" -StaticMacAddress "{data["mac_address"]}"; '
125+
f'Set-VMFirmware -VMName "{vm_name}" -EnableSecureBoot Off; '
126+
)
127+
run_powershell_ssh_int(command, data["url"], data["username"], data["password"])
120128

121-
run_powershell_ssh(f'Start-VM -Name "{data["virtualmachinename"]}"', data["url"], data["username"], data["password"])
129+
# Switch should have vlan support (External/Internal)
130+
# run_powershell_ssh_int(f'Set-VMNetworkAdapterVlan -VMName "{"vm_name"}" -Access -VlanId "{data["cloudstack.vlan"]}"', data["url"], data["username"], data["password"])
131+
run_powershell_ssh_int(f'Start-VM -Name "{vm_name}"', data["url"], data["username"], data["password"])
132+
vm_started = True
122133

123-
succeed({"status": "success", "message": "Instance created"})
134+
succeed({"status": "success", "message": "Instance created"})
124135

136+
except Exception as e:
137+
if vm_started:
138+
run_powershell_ssh_int(f'Stop-VM -Name "{vm_name}" -Force -TurnOff', data["url"], data["username"], data["password"])
139+
if vm_created:
140+
run_powershell_ssh_int(f'Remove-VM -Name "{vm_name}" -Force', data["url"], data["username"], data["password"])
141+
if vhd_created:
142+
run_powershell_ssh_int(f'Remove-Item -Path \\"{vhd_path}\\" -Force', data["url"], data["username"], data["password"])
143+
fail(str(e))
125144

126145
def start(data):
127146
run_powershell_ssh(f'Start-VM -Name "{data["virtualmachinename"]}"', data["url"], data["username"], data["password"])
@@ -138,21 +157,6 @@ def reboot(data):
138157
succeed({"status": "success", "message": "Instance rebooted"})
139158

140159

141-
def pause(data):
142-
run_powershell_ssh(f'Suspend-VM -Name "{data["virtualmachinename"]}"', data["url"], data["username"], data["password"])
143-
succeed({"status": "success", "message": "Instance paused"})
144-
145-
146-
def resume(data):
147-
run_powershell_ssh(f'Resume-VM -Name "{data["virtualmachinename"]}"', data["url"], data["username"], data["password"])
148-
succeed({"status": "success", "message": "Instance resumed"})
149-
150-
151-
def delete(data):
152-
run_powershell_ssh(f'Remove-VM -Name "{data["virtualmachinename"]}" -Force', data["url"], data["username"], data["password"])
153-
succeed({"status": "success", "message": "Instance deleted"})
154-
155-
156160
def status(data):
157161
command = f'(Get-VM -Name "{data["virtualmachinename"]}").State'
158162
state = run_powershell_ssh(command, data["url"], data["username"], data["password"])
@@ -164,25 +168,82 @@ def status(data):
164168
power_state = "unknown"
165169
succeed({"status": "success", "power_state": power_state})
166170

171+
172+
def delete(data):
173+
run_powershell_ssh(f'Remove-VM -Name "{data["virtualmachinename"]}" -Force', data["url"], data["username"], data["password"])
174+
succeed({"status": "success", "message": "Instance deleted"})
175+
176+
177+
def suspend(data):
178+
run_powershell_ssh(f'Suspend-VM -Name "{data["virtualmachinename"]}"', data["url"], data["username"], data["password"])
179+
succeed({"status": "success", "message": "Instance suspended"})
180+
181+
182+
def resume(data):
183+
run_powershell_ssh(f'Resume-VM -Name "{data["virtualmachinename"]}"', data["url"], data["username"], data["password"])
184+
succeed({"status": "success", "message": "Instance resumed"})
185+
186+
187+
def create_snapshot(data):
188+
snapshot_name = data["snapshot_name"]
189+
if snapshot_name == "":
190+
fail("Missing snapshot_name in parameters")
191+
command = f'Checkpoint-VM -VMName \\"{data["virtualmachinename"]}\\" -SnapshotName \\"{snapshot_name}\\"'
192+
run_powershell_ssh(command, data["url"], data["username"], data["password"])
193+
succeed({"status": "success", "message": f"Snapshot '{snapshot_name}' created"})
194+
195+
196+
def restore_snapshot(data):
197+
snapshot_name = data["snapshot_name"]
198+
if snapshot_name == "":
199+
fail("Missing snapshot_name in parameters")
200+
command = f'Restore-VMSnapshot -VMName \\"{data["virtualmachinename"]}\\" -Name \\"{snapshot_name}\\" -Confirm:$false'
201+
run_powershell_ssh(command, data["url"], data["username"], data["password"])
202+
succeed({"status": "success", "message": f"Snapshot '{snapshot_name}' restored"})
203+
204+
205+
def delete_snapshot(data):
206+
snapshot_name = data["snapshot_name"]
207+
if snapshot_name == "":
208+
fail("Missing snapshot_name in parameters")
209+
command = f'Remove-VMSnapshot -VMName \\"{data["virtualmachinename"]}\\" -Name \\"{snapshot_name}\\" -Confirm:$false'
210+
run_powershell_ssh(command, data["url"], data["username"], data["password"])
211+
succeed({"status": "success", "message": f"Snapshot '{snapshot_name}' deleted"})
212+
213+
167214
def parse_json(json_data):
168215
try:
169216
data = {
170-
"url": json_data["external"]["extensionid"]["url"],
171-
"username": json_data["external"]["extensionid"]["user"],
172-
"password": json_data["external"]["extensionid"]["secret"],
173-
"switch_name": json_data["external"]["hostid"].get("switch_name", "Default Switch"),
174-
"default_vhd_path": json_data["external"]["hostid"].get("default_vhd_path", "C:\\ProgramData\\Microsoft\\Windows\\Virtual Hard Disks"),
175-
"default_vm_path": json_data["external"]["hostid"].get("default_vm_path", "C:\\ProgramData\\Microsoft\\Windows\\Hyper-V"),
176-
"template_type": json_data["external"]["virtualmachineid"].get("template_type", "template"),
177-
"template_path": json_data["external"]["virtualmachineid"].get("template_path", ""),
178-
"vhd_size_gb": json_data["external"]["virtualmachineid"].get("vhd_size_gb", 25),
179-
"iso_path": json_data["external"]["virtualmachineid"].get("iso_path", ""),
180-
"generation": json_data["external"]["virtualmachineid"].get("generation", 2),
181-
"virtualmachinename": json_data["cloudstack.vm.details"]["name"],
182-
"cpus": json_data["cloudstack.vm.details"].get("cpus", 2),
183-
"memory": json_data["cloudstack.vm.details"].get("minRam", 536870912),
184-
"mac_address": json_data["cloudstack.vm.details"]["nics"][0]["mac"]
217+
"url": json_data["externaldetails"]["extension"]["url"],
218+
"username": json_data["externaldetails"]["extension"]["username"],
219+
"password": json_data["externaldetails"]["extension"]["password"],
220+
"virtualmachinename": json_data["cloudstack.vm.details"]["name"]
185221
}
222+
223+
external_host_details = json_data["externaldetails"].get("host", [])
224+
data["switch_name"] = external_host_details.get("switch_name", "Default Switch")
225+
data["default_vhd_path"] = external_host_details.get("default_vhd_path", "C:\\ProgramData\\Microsoft\\Windows\\Virtual Hard Disks")
226+
data["default_vm_path"] = external_host_details.get("default_vm_path", "C:\\ProgramData\\Microsoft\\Windows\\Hyper-V")
227+
228+
external_vm_details = json_data["externaldetails"].get("virtualmachine", [])
229+
if external_vm_details:
230+
data["template_type"] = external_vm_details.get("template_type", "template")
231+
data["template_path"] = external_vm_details.get("template_path", "")
232+
data["vhd_size_gb"] = external_vm_details.get("vhd_size_gb", 25)
233+
data["iso_path"] = external_vm_details.get("iso_path", "")
234+
data["generation"] = external_vm_details.get("generation", 2)
235+
236+
data["cpus"] = json_data["cloudstack.vm.details"].get("cpus", 2)
237+
data["memory"] = json_data["cloudstack.vm.details"].get("minRam", 536870912)
238+
239+
nics = json_data["cloudstack.vm.details"].get("nics", [])
240+
if nics:
241+
data["mac_address"] = nics[0].get("mac", "")
242+
243+
parameters = json_data.get("parameters", [])
244+
if parameters:
245+
data["snapshot_name"] = parameters.get("snapshot_name", "")
246+
186247
return data
187248
except KeyError as e:
188249
fail(f"Missing required field in JSON: {str(e)}")
@@ -211,10 +272,13 @@ def main():
211272
"start": start,
212273
"stop": stop,
213274
"reboot": reboot,
214-
"pause": pause,
215-
"resume": resume,
216275
"delete": delete,
217276
"status": status,
277+
"suspend": suspend,
278+
"resume": resume,
279+
"createsnapshot": create_snapshot,
280+
"restoresnapshot": restore_snapshot,
281+
"deletesnapshot": delete_snapshot
218282
}
219283

220284
if operation not in operations:

0 commit comments

Comments
 (0)