From cc74e4260103228da1362b95f97b610375b1fe0e Mon Sep 17 00:00:00 2001 From: Leonid Belenkiy Date: Wed, 29 May 2024 10:29:09 +0200 Subject: [PATCH] Add new vmtools upgrade workflow Signed-off-by: Leonid Belenkiy --- .DS_Store | Bin 8196 -> 8196 bytes upgrade_vm_tools/pom.xml | 2 +- upgrade_vm_tools/src/actions/actionSample.js | 18 -- upgrade_vm_tools/src/actions/functions.ts | 39 ++-- .../src/license/THIRD-PARTY.properties | 2 +- upgrade_vm_tools/src/test/functions.test.ts | 64 ------ .../workflows/upgrade_vm_tools.wf.form.json | 183 ++++++++++++------ .../src/workflows/upgrade_vm_tools.wf.ts | 97 +++++----- 8 files changed, 192 insertions(+), 213 deletions(-) delete mode 100644 upgrade_vm_tools/src/actions/actionSample.js diff --git a/.DS_Store b/.DS_Store index 3fbb65add7d7ae3b73e36e904676b4087ba2abe5..2a612685809169f6c404c603b7e4a8a31c7f3edb 100644 GIT binary patch delta 216 zcmZp1XmQw}DiFuKxSWB3frUYjA)O(Up(Hoo#U&{xKM5$t!EKqu`H|_EBdUA~UipFy z!{Frn+ybB;1_rSUlbZz=F@|qe6O>_LVHY^$JGo9&ZE}t%7klePwL|+Dk4>%>HAG^o bPQE3|!_2Yu!Nx>6rp@dU)~qDzl4b$`P`^M; delta 216 zcmZp1XmQw}DiFuSe2amBfrUYjA)O(Up(Hoo#U&{xKM5$t5iWl_e8Q`vj;Qh}c;yQ+ z41<&Na|?ia7#R2!CN~Q#VszQ8CMd(g5^*V4XL6mW+T^c8X~b( aC*Km~VU8-com.clouddepth upgrade_vm_tools - 1.0.73 + 1.0.82 package diff --git a/upgrade_vm_tools/src/actions/actionSample.js b/upgrade_vm_tools/src/actions/actionSample.js deleted file mode 100644 index f268f16..0000000 --- a/upgrade_vm_tools/src/actions/actionSample.js +++ /dev/null @@ -1,18 +0,0 @@ -/*- - * #%L - * upgrade_vm_tools - * %% - * Copyright (C) 2024 TODO: Enter Organization name - * %% - * TODO: Define header text - * #L% - */ -/** - * @param {number} a - * @param {number} b - * - * @return {number} - */ -(function (a, b) { - return a + b; -}); diff --git a/upgrade_vm_tools/src/actions/functions.ts b/upgrade_vm_tools/src/actions/functions.ts index 9ae39fd..dbd95e8 100644 --- a/upgrade_vm_tools/src/actions/functions.ts +++ b/upgrade_vm_tools/src/actions/functions.ts @@ -12,7 +12,6 @@ export class Functions { * upgradePoweredOfVMs */ - //TODO: rename it to upgradePoweredOfVM public isUpgradePoweredOffVmAllowed(allowUpgradePoweredOffVm: boolean, vm: VcVirtualMachine): boolean { if (allowUpgradePoweredOffVm || vm.runtime.powerState.value === "poweredOn") return true; System.warn(`The current state '${vm.runtime.powerState.value}' is prohibited for the upgrade.`); @@ -224,19 +223,24 @@ export class Functions { System.getModule("com.vmware.library.vc.vm.tools").vim3WaitToolsStarted(vm, 2, vmtoolsTimeout); } catch (e) { const toolsStatus = vm.guest.toolsStatus.value; - throw new Error(`VMTools status: ${toolsStatus}. ${e}`); + throw new Error(`VMTools status: ${toolsStatus}: ${e}`); } } public upgradeVmTools({ vm, allowReboot = false, waitForTools }: { vm: VcVirtualMachine; allowReboot: boolean; waitForTools: boolean }): boolean { - const toolsStatus = vm.guest.toolsStatus.value; - + const toolsStatus = vm.guest.toolsVersionStatus2; + System.log("tools status: " + toolsStatus); switch (toolsStatus) { - case "toolsOk": + case "guestToolsSupportedNew": + System.log("VMtools are newer than available. Skipping the upgrade"); + case "guestToolsUnmanaged": + System.log("3rd party managed VMtools (open-vm-tools). Skipping the upgrade"); + case "guestToolsCurrent": System.log("VMware Tools are already running and up to date. Nothing to do."); return true; - case "toolsOld": - case "toolsNotRunning": + case "guestToolsBlacklisted": + case "guestToolsNeedUpgrade": + case "guestToolsSupportedOld": System.log("Starting VMware Tools upgrade..."); const upgradeArgs = allowReboot ? "/s /vqn" : '/s /v"/qn REBOOT=ReallySuppress"'; try { @@ -249,14 +253,14 @@ export class Functions { } return true; } catch (e) { - throw new Error(`Failed to upgrade VMware Tools. ${e}`); + throw new Error(`Failed to upgrade VMware Tools: ${e}`); } - case "toolsNotInstalled": - throw new Error("Unable to upgrade VMware Tools because they are not currently installed."); + case "guestToolsNotInstalled": + throw new Error(`Unable to upgrade VMware Tools because they are not currently installed.`); default: - throw new Error("Unexpected VMware Tools status: " + toolsStatus); + throw new Error(`Unexpected VMware Tools status: ${toolsStatus}`); } } @@ -268,7 +272,6 @@ export class Functions { } System.log(`Current VMTools upgrade policy is '${currentVmToolsUpgradePolicy}'. Updating to '${desiredVmToolsUpgradePolicy}'.`); - const configSpec = new VcVirtualMachineConfigSpec(); const toolsSpec = new VcToolsConfigInfo(); toolsSpec.toolsUpgradePolicy = desiredVmToolsUpgradePolicy; @@ -279,7 +282,7 @@ export class Functions { System.getModule("com.vmware.library.vc.basic").vim3WaitTaskEnd(task, true, 2); System.log(`Successfully set VMTools upgrade policy to '${desiredVmToolsUpgradePolicy}'.`); } catch (e) { - throw new Error(`Failed to set VMTools upgrade policy to '${desiredVmToolsUpgradePolicy}'. ${e}`); + throw new Error(`Failed to set VMTools upgrade policy to '${desiredVmToolsUpgradePolicy}': ${e}`); } } @@ -288,11 +291,11 @@ export class Functions { throw new Error("Required parameters are missing"); } try { - const task = vm.createSnapshot_Task(name, description, memory, quiesce); + const task: VcTask = vm.createSnapshot_Task(name, description, memory, quiesce); System.getModule("com.vmware.library.vc.basic").vim3WaitTaskEnd(task, true, 2); System.log(`Creating snapshot '${name}' on VM '${vm.name} was completed successfully'`); } catch (error) { - throw new Error(`Failed to create snapshot '${name}' on VM '${vm.name}'. ${error}`); + throw new Error(`Failed to create snapshot '${name}' on VM '${vm.name}': ${error}`); } } @@ -312,16 +315,16 @@ export class Functions { } public removeSnapshot({ vm, removeChildren, consolidate, snapshotName }: { vm: VcVirtualMachine; removeChildren: boolean; consolidate: boolean; snapshotName: string }): void { - const vmSnapshots = this.getVmSnapshot(vm); + const vmSnapshots: VcVirtualMachineSnapshot[] = this.getVmSnapshot(vm); if (!vmSnapshots) return; vmSnapshots.forEach((snapshot) => { if (snapshot.name === snapshotName) { try { const task = snapshot.removeSnapshot_Task(removeChildren, consolidate); System.getModule("com.vmware.library.vc.basic").vim3WaitTaskEnd(task, true, 2); - System.log(`Snapshot '${snapshot.name}' was removed successfully`); + System.log(`Snapshot '${snapshotName}' was removed successfully`); } catch (error) { - const errorMessage = `Failed to remove snapshot '${snapshot.name}' on VM '${vm.name}': ${error instanceof Error ? error.message : String(error)}`; + const errorMessage = `Failed to remove snapshot '${snapshotName}' on VM '${vm.name}': ${error instanceof Error ? error.message : String(error)}`; throw new Error(errorMessage); } } diff --git a/upgrade_vm_tools/src/license/THIRD-PARTY.properties b/upgrade_vm_tools/src/license/THIRD-PARTY.properties index e36bb8a..e0b047f 100644 --- a/upgrade_vm_tools/src/license/THIRD-PARTY.properties +++ b/upgrade_vm_tools/src/license/THIRD-PARTY.properties @@ -17,6 +17,6 @@ # Please fill the missing licenses for dependencies : # # -#Fri May 24 19:35:16 CEST 2024 +#Sun May 26 16:23:58 CEST 2024 classworlds--classworlds--1.1-alpha-2= org.codehaus.plexus--plexus-container-default--1.0-alpha-9-stable-1= diff --git a/upgrade_vm_tools/src/test/functions.test.ts b/upgrade_vm_tools/src/test/functions.test.ts index 5abbc6a..40bd591 100644 --- a/upgrade_vm_tools/src/test/functions.test.ts +++ b/upgrade_vm_tools/src/test/functions.test.ts @@ -18,7 +18,6 @@ describe("isUpgradePoweredOffVmAllowed", () => { const vm = { runtime: { powerState: { value: "poweredOn" } } }; const allowUpgradePoweredOffVm = true; //@ts-ignore - //TODO: convert 'vm' to be of type VCVirtualMachine const result = func.isUpgradePoweredOffVmAllowed(allowUpgradePoweredOffVm, vm); expect(result).toBe(true); }); @@ -27,7 +26,6 @@ describe("isUpgradePoweredOffVmAllowed", () => { const vm = { runtime: { powerState: { value: "poweredOff" } } }; const allowUpgradePoweredOffVm = true; //@ts-ignore - //TODO: convert 'vm' to be of type VCVirtualMachine const result = func.isUpgradePoweredOffVmAllowed(allowUpgradePoweredOffVm, vm); expect(result).toBe(true); }); @@ -37,7 +35,6 @@ describe("isUpgradePoweredOffVmAllowed", () => { const vm = { runtime: { powerState: { value: "poweredOff" } } }; const allowUpgradePoweredOffVm = false; //@ts-ignore - //TODO: convert 'vm' to be of type VCVirtualMachine const result = func.isUpgradePoweredOffVmAllowed(allowUpgradePoweredOffVm, vm); expect(result).toBe(false); expect(spy).toHaveBeenCalledWith(`The current state '${vm.runtime.powerState.value}' is prohibited for the upgrade.`); @@ -212,67 +209,6 @@ describe("isUpgradeVmTemplatesAllowed", () => { }); }); -//TODO: fix it -// describe("getVmDiskMode", () => { -// beforeEach(() => { -// func = new Functions(); -// (VcVirtualDiskFlatVer2BackingInfo) = { -// device: function () {} -// }; -// }); -// it("should return the disk device if found", () => { -// const mockVm = { -// config: { -// hardware: { -// device: [ -// { -// /* Other device */ -// }, -// { -// /* Another device */ -// }, -// { instanceof: VcVirtualDiskFlatVer2BackingInfo } // Mock disk device -// ] -// } -// }, -// name: "MyVM" -// }; -// //@ts-ignore -// const result = func.getVmDiskMode(mockVm); -// //@ts-ignore -// expect(result).toBe(mockVm.config.hardware.device[2]); // Verify returned device -// }); - -// it("should return null if no disks are found", () => { -// const mockVm = { -// config: { -// hardware: { -// device: [ -// { -// /* Other device */ -// } -// ] -// } -// }, -// name: "MyVM" -// }; -// const spy = spyOn(System, "log"); -// //@ts-ignore -// const result = func.getVmDiskMode(mockVm); -// expect(result).toBeNull(); -// //expect(console.log).toHaveBeenCalledWith(`No disks found for virtual machine '${mockVm.name}'`); // Verify log message -// }); - -// it("should return null if VM configuration or hardware information is missing", () => { -// const mockVm = { config: null, name: "MyVM" }; -// //@ts-ignore -// const result = func.getVmDiskMode(mockVm); -// const spy = spyOn(System, "log"); -// expect(result).toBeNull(); -// // expect(console.log).toHaveBeenCalledWith(`VM configuration or hardware information missing for '${mockVm.name}'`); // Verify log message -// }); -// }); - describe("setVmToolsUpgradePolicy", () => { let vm: any; let System: any; diff --git a/upgrade_vm_tools/src/workflows/upgrade_vm_tools.wf.form.json b/upgrade_vm_tools/src/workflows/upgrade_vm_tools.wf.form.json index 13ba318..0804c8c 100644 --- a/upgrade_vm_tools/src/workflows/upgrade_vm_tools.wf.form.json +++ b/upgrade_vm_tools/src/workflows/upgrade_vm_tools.wf.form.json @@ -16,16 +16,7 @@ "id": "section_0" }, { - "fields": [ - { - "display": "textField", - "id": "__tokenName", - "signpostPosition": "right-middle", - "state": { - "visible": false - } - } - ], + "fields": [], "id": "section_1" }, { @@ -33,7 +24,11 @@ { "display": "checkbox", "id": "allowReboot", - "signpostPosition": "right-middle" + "signpostPosition": "right-middle", + "state": { + "read-only": false, + "visible": true + } } ], "id": "section_2" @@ -43,7 +38,11 @@ { "display": "checkbox", "id": "setVmToolsUpgradePolicy", - "signpostPosition": "right-middle" + "signpostPosition": "right-middle", + "state": { + "read-only": false, + "visible": true + } } ], "id": "section_3" @@ -55,24 +54,15 @@ "id": "desiredVmToolsUpgradePolicy", "signpostPosition": "right-middle", "state": { - "visible": { - "id": "com.vmware.o11n.forms/evalOGNL", - "parameters": [ - { - "ognl": "`#setVmToolsUpgradePolicy`" - }, - { - "setVmToolsUpgradePolicy": "setVmToolsUpgradePolicy" - }, - { - "setVmToolsUpgradePolicy__type_": "`boolean`" + "read-only": false, + "visible": [ + { + "equals": { + "setVmToolsUpgradePolicy": true }, - { - "return": "`boolean`" - } - ], - "type": "scriptAction" - } + "value": true + } + ] } } ], @@ -123,9 +113,67 @@ "read-only": false, "visible": true } + }, + { + "display": "textField", + "id": "snapshotName", + "signpostPosition": "right-middle", + "state": { + "read-only": false, + "visible": [ + { + "equals": { + "createSnapshot": true + }, + "value": true + } + ] + } } ], "id": "section_586819e2" + }, + { + "fields": [ + { + "display": "checkbox", + "id": "removeChildren", + "signpostPosition": "right-middle", + "state": { + "read-only": false, + "visible": [ + { + "equals": { + "createSnapshot": true + }, + "value": true + } + ] + } + } + ], + "id": "section_d2b2e75a" + }, + { + "fields": [ + { + "display": "checkbox", + "id": "consolidate", + "signpostPosition": "right-middle", + "state": { + "read-only": false, + "visible": [ + { + "equals": { + "createSnapshot": true + }, + "value": true + } + ] + } + } + ], + "id": "section_8c76f1ff" } ], "title": "General" @@ -136,38 +184,12 @@ "externalValidations": [] }, "schema": { - "__tokenName": { - "constraints": {}, - "default": { - "id": "com.vmware.o11n.forms/evalOGNL", - "parameters": [ - { - "ognl": "`#vm.name`" - }, - { - "vm": "vm" - }, - { - "vm__type_": "`VC:VirtualMachine`" - }, - { - "return": "`string`" - } - ], - "type": "scriptAction" - }, - "id": "__tokenName", - "label": "Internal property", - "type": { - "dataType": "string", - "isMultiple": false - } - }, "allowReboot": { "constraints": {}, - "default": true, + "default": false, "id": "allowReboot", - "label": "Allow reboot? If true, VM will be rebooted if required by the installation. If false, reboot will be suppressed.", + "label": "Allow reboot?", + "signpost": "If true, VM will be rebooted if required by the installation. If false, reboot will be suppressed", "type": { "dataType": "boolean", "isMultiple": false @@ -184,7 +206,7 @@ } ], "id": "allowUpgradePoweredOffVms", - "label": "allowUpgradePoweredOffVms", + "label": "Upgrade Powered Off VMs?", "type": { "dataType": "boolean", "isMultiple": false @@ -194,15 +216,26 @@ "constraints": {}, "default": false, "id": "allowUpgradeTemplates", - "label": "allowUpgradeTemplates", + "label": "Upgrade VM Templates?", + "signpost": "Template will be converted to VM and converted back to template", "type": { "dataType": "boolean", "isMultiple": false } }, + "consolidate": { + "default": true, + "description": "Consolidate snapshot", + "label": "Consolidate snapshot when deleting?", + "type": { + "dataType": "boolean" + } + }, "createSnapshot": { + "default": true, "description": "Create a snapshot before any changes", - "label": "createSnapshot", + "label": "Create snapshot?", + "signpost": "Snapshot will be created and deleted if upgrade will be successful. ", "type": { "dataType": "boolean" } @@ -230,12 +263,28 @@ }, "default": "manual", "id": "desiredVmToolsUpgradePolicy", - "label": "Desired VMTools upgrade policy (manual or upgradeAtPowerCycle)", + "label": "Desired VMTools upgrade policy", "type": { "dataType": "string", "isMultiple": false }, - "valueList": ["manual", "upgradeAtPowerCycle"] + "valueList": [ + { + "label": "Manual", + "value": "manual" + }, + { + "label": "Upgrade at power cycle", + "value": "upgradeAtPowerCycle" + } + ] + }, + "removeChildren": { + "description": "Remove children snapshots", + "label": "Remove children snapshot?", + "type": { + "dataType": "boolean" + } }, "setVmToolsUpgradePolicy": { "constraints": {}, @@ -247,6 +296,14 @@ "isMultiple": false } }, + "snapshotName": { + "default": "upgrade_vm_tools", + "description": "Snapshot name", + "label": "Snapshot Name", + "type": { + "dataType": "string" + } + }, "vm": { "constraints": { "required": true diff --git a/upgrade_vm_tools/src/workflows/upgrade_vm_tools.wf.ts b/upgrade_vm_tools/src/workflows/upgrade_vm_tools.wf.ts index b9acff4..8ecbe2e 100644 --- a/upgrade_vm_tools/src/workflows/upgrade_vm_tools.wf.ts +++ b/upgrade_vm_tools/src/workflows/upgrade_vm_tools.wf.ts @@ -23,10 +23,6 @@ import { Functions } from "../actions/functions"; required: true, title: "Foo" }, - __tokenName: { - type: "string", - description: "Internal property" - }, allowReboot: { type: "boolean", description: "Allow reboot? If true, VM will be rebooted if required by the installation. If false, reboot will be suppressed." @@ -58,6 +54,10 @@ import { Functions } from "../actions/functions"; consolidate: { type: "boolean", description: "Consolidate snapshot" + }, + snapshotName: { + type: "string", + description: "Snapshot name" } }, output: { @@ -68,7 +68,6 @@ import { Functions } from "../actions/functions"; export class UpgradeVMTools { public install( vm: VcVirtualMachine, - __tokenName: string, allowReboot: boolean, setVmToolsUpgradePolicy: boolean, desiredVmToolsUpgradePolicy: string, @@ -77,13 +76,13 @@ export class UpgradeVMTools { createSnapshot: boolean, removeChildren: boolean, consolidate: boolean, + snapshotName: string, @Out result: any ): void { const func = new Functions(); const shutdownTimeout = 10; // seconds const vmtoolsTimeout = 10; // minutes const waitForTools = true; - const snapshotName = "upgrade_vm_tools"; const snapshotDescription = `Upgrade VM Tools. Created by ${Server.getCurrentLdapUser()}`; const snapshotWithMemory = false; const snapshotWithQuiesce = false; @@ -102,50 +101,52 @@ export class UpgradeVMTools { const initialPowerState = func.getVmPowerState(vm); if (!allowUpgradePoweredOffVms && initialPowerState === "poweredOff") throw new Error("VM is powered off and not allowed for upgrade"); if (!allowUpgradeTemplates && isVmTemplate) throw new Error("VM is template and templates are not allowed for upgrade"); - if (createSnapshot) func.createVmSnapshot(vars); - // if (isVmTemplate) { - // const currentHostSystem = func.getVmParentHost(vm); - // const currentComputeResource = func.getComputeResource(currentHostSystem); - // //@ts-ignore - // const currentResourcePool = func.getResourcePool(currentComputeResource); - // const vars = { - // vm: vm, - // pool: currentResourcePool, - // host: currentHostSystem - // }; - // func.convertTemplateToVm(vars); - // } - // const vmDisks = func.getVmDisks(vm); - // if (!vmDisks) throw new Error(`No disks found for virtual machine '${vm.name}'`); - // const isDiskNonPersistent = func.getVmNonPersistentDisks(vmDisks, diskPersistencyType.persistent).length !== 0; - // if (isDiskNonPersistent) { - // System.log(`Preparing disks for conversion to ${diskPersistencyType.persistent}`); - // func.shutdownVmBasedOnCurrentState(vm, shutdownTimeout); - // if (vm.snapshot != null) throw new Error("Disks cannot be converted because the virtual machine has at least one snapshot"); - // const diskPersistency = func.prepareVmDiskPersistency(vmDisks, diskPersistencyType.persistent); - // func.changeVmDiskPersistency(diskPersistency, vm); - // } + if (isVmTemplate) { + const currentHostSystem = func.getVmParentHost(vm); + const currentComputeResource = func.getComputeResource(currentHostSystem); + //@ts-ignore + const currentResourcePool = func.getResourcePool(currentComputeResource); + const vars = { + vm: vm, + pool: currentResourcePool, + host: currentHostSystem + }; + func.convertTemplateToVm(vars); + } + if (createSnapshot) func.createVmSnapshot(vars); + const vmDisks = func.getVmDisks(vm); + if (!vmDisks) throw new Error(`No disks found for virtual machine '${vm.name}'`); + const isDiskNonPersistent = func.getVmNonPersistentDisks(vmDisks, diskPersistencyType.persistent).length !== 0; + if (isDiskNonPersistent) { + System.log(`Preparing disks for conversion to ${diskPersistencyType.persistent}`); + func.shutdownVmBasedOnCurrentState(vm, shutdownTimeout); + if (vm.snapshot != null) throw new Error("Disks cannot be converted because the virtual machine has at least one snapshot"); + const diskPersistency: VcVirtualMachineConfigSpec = func.prepareVmDiskPersistency(vmDisks, diskPersistencyType.persistent); + func.changeVmDiskPersistency(diskPersistency, vm); + } - // if (initialPowerState !== "poweredOn") func.powerOnVm(vm); - // func.checkVmToolsStatus(vm, vmtoolsTimeout); - // func.upgradeVmTools({ vm, allowReboot, waitForTools }); - // func.checkVmToolsStatus(vm, vmtoolsTimeout); + if (initialPowerState !== "poweredOn") func.powerOnVm(vm); + func.checkVmToolsStatus(vm, vmtoolsTimeout); + func.upgradeVmTools({ vm, allowReboot, waitForTools }); + func.checkVmToolsStatus(vm, vmtoolsTimeout); - // if (setVmToolsUpgradePolicy) func.setVmToolsUpgradePolicy(vm, desiredVmToolsUpgradePolicy); - // if (initialPowerState === "poweredOff") func.handlePoweredOnVm(vm, shutdownTimeout); - // if (initialPowerState === "suspended") func.handleSuspendedVm(vm); - // if (isDiskNonPersistent) { - // const diskPersistency: VcVirtualMachineConfigSpec = func.prepareVmDiskPersistency(vmDisks, diskPersistencyType.nonpersistent); - // func.changeVmDiskPersistency(diskPersistency, vm); - // } - // if (isVmTemplate) func.convertVmToTemplate(vm); - const snapshotVars = { - vm: vm, - removeChildren: removeChildren, - consolidate: consolidate, - snapshotName: snapshotName - }; - func.removeSnapshot(snapshotVars); + if (setVmToolsUpgradePolicy) func.setVmToolsUpgradePolicy(vm, desiredVmToolsUpgradePolicy); + if (initialPowerState === "poweredOff") func.handlePoweredOnVm(vm, shutdownTimeout); + if (initialPowerState === "suspended") func.handleSuspendedVm(vm); + if (isDiskNonPersistent) { + const diskPersistency: VcVirtualMachineConfigSpec = func.prepareVmDiskPersistency(vmDisks, diskPersistencyType.nonpersistent); + func.changeVmDiskPersistency(diskPersistency, vm); + } + if (createSnapshot) { + const snapshotVars = { + vm: vm, + removeChildren: removeChildren, + consolidate: consolidate, + snapshotName: snapshotName + }; + func.removeSnapshot(snapshotVars); + } + if (isVmTemplate) func.convertVmToTemplate(vm); } }