mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-08-16 12:49:14 +00:00
feat: allow Cloud Config for Windows guests (Cloudbase-Init) (#984)
* feat(vm): allow Cloud Config for Windows guests Currently the Cloud Config editor is completely hidden when a VM's OS Type is set to Windows, and no `cloudinitdisk` volume or secret is written to the VM spec even if the user has entered content. This blocks a common Windows-on-Harvester workflow: using Cloudbase-Init to run first-boot configuration (install VMDP, enable OpenSSH server, add authorized_keys, join a domain, run `runcmd` steps, etc.). Users on Windows today have to hand-edit the VM YAML after creation to attach a cloudinitdisk, which is fragile and undiscoverable. Changes: - Remove the `v-if="!isWindows"` on the CloudConfig component so the editor is available for every OS type. The Sysprep editor continues to render only for Windows, so Windows now gets both panels; Linux is unchanged. - Drop the `if (!this.isWindows)` guard around the cloudinitdisk volume serialization so the disk is attached whenever the user provided user-data or network-data, regardless of OS. - Drop the `|| this.isWindows` early-return in `saveSecret` so the cloud-init secret is persisted for Windows VMs too. - `getInitUserData` now returns a bare `#cloud-config\n` header for Windows instead of the Linux qemu-guest-agent runcmd template (which would fail on Cloudbase-Init). VMDP installs the QGA on Windows as a native service, so the runcmd path is not needed. The "Install guest agent" checkbox stays disabled for Windows because it specifically drives the Linux QGA-via-runcmd recipe. Fixes point 2 of harvester/harvester#11124. Signed-off-by: Alejandro Bonilla <abonilla@suse.com> * fix(vm): do not clear cloud-config on isWindows watcher fire The isWindows watcher runs on every change of the isWindows computed property, including on mount when editing an existing Windows VM. Setting this['userScript'] and this['networkScript'] to undefined at that point wipes the cloud-config data that was just loaded from the existing VM's cloudinit secret, so the Advanced -> Cloud Config editor shows empty even though the VM has data on the wire. Remove the two clears - Cloud Config is now supported for Windows guests by this PR, so there is no reason to unset the user/network scripts when the OS type is Windows. sshKey and installAgent remain gated (SSH keys are injected via cloud-init on Linux only, and the qemu-guest-agent package install checkbox does not apply to Windows). Signed-off-by: Alejandro Bonilla <abonilla@suse.com> Co-authored-by: Volker Theile <vtheile@suse.com> * refactor(vm): Get CloudInit working for VM templates - Remove unnecessary osType watcher in cloud config. The watcher (see `pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineCloudConfig/DataTemplate.vue`) that cleared the cloud config template for Windows VMs is no longer necessary. - Automatically strip the CloudInit `User Data` properties `package_update` and `packages` for OS type `Windows`. Signed-off-by: Volker Theile <vtheile@suse.com> --------- Signed-off-by: Alejandro Bonilla <abonilla@suse.com> Signed-off-by: Volker Theile <vtheile@suse.com> Co-authored-by: Volker Theile <vtheile@suse.com>
This commit is contained in:
parent
d23c3d3b72
commit
e013534373
@ -68,12 +68,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
osType(neu) {
|
|
||||||
if (neu === 'windows') {
|
|
||||||
this.id = '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
value(neu) {
|
value(neu) {
|
||||||
this.yamlScript = neu;
|
this.yamlScript = neu;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1062,7 +1062,6 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CloudConfig
|
<CloudConfig
|
||||||
v-if="!isWindows"
|
|
||||||
ref="yamlEditor"
|
ref="yamlEditor"
|
||||||
:user-script="userScript"
|
:user-script="userScript"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
|
|||||||
@ -917,7 +917,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!disks.find( (D) => D.name === 'cloudinitdisk') && (this.userData || this.networkScript)) {
|
if (!disks.find( (D) => D.name === 'cloudinitdisk') && (this.userData || this.networkScript)) {
|
||||||
if (!this.isWindows) {
|
|
||||||
disks.push({
|
disks.push({
|
||||||
name: 'cloudinitdisk',
|
name: 'cloudinitdisk',
|
||||||
disk: { bus: 'virtio' }
|
disk: { bus: 'virtio' }
|
||||||
@ -943,7 +942,6 @@ export default {
|
|||||||
|
|
||||||
volumes.push(cloudinitdisk);
|
volumes.push(cloudinitdisk);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const specDisks = this.spec?.template?.spec?.domain?.devices?.disks;
|
const specDisks = this.spec?.template?.spec?.domain?.devices?.disks;
|
||||||
const mergedDisks = this.mergeDeviceList(specDisks, disks);
|
const mergedDisks = this.mergeDeviceList(specDisks, disks);
|
||||||
@ -1144,6 +1142,16 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getInitUserData(config) {
|
getInitUserData(config) {
|
||||||
|
// Windows guests don't use qemu-guest-agent via systemd (VMDP installs
|
||||||
|
// the QGA as a Windows service), and the Linux `runcmd` recipe would
|
||||||
|
// fail on Cloudbase-Init. Return an empty string so `this.userData`
|
||||||
|
// stays falsy on Windows until the user actually enters cloud-config
|
||||||
|
// content — this prevents emitting a spurious cloudinitdisk volume
|
||||||
|
// and Secret for every Windows VM (Copilot review comment on #984).
|
||||||
|
if (config.osType === 'windows') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
const _QGA_JSON = this.getMatchQGA(config.osType);
|
const _QGA_JSON = this.getMatchQGA(config.osType);
|
||||||
|
|
||||||
const out = jsyaml.dump(_QGA_JSON);
|
const out = jsyaml.dump(_QGA_JSON);
|
||||||
@ -1346,13 +1354,22 @@ export default {
|
|||||||
*/
|
*/
|
||||||
deleteYamlDocProp(doc, paths) {
|
deleteYamlDocProp(doc, paths) {
|
||||||
try {
|
try {
|
||||||
const item = doc.getIn([])?.items[0];
|
const items = doc.getIn([])?.items;
|
||||||
const key = item?.key;
|
const firstKey = items?.[0]?.key;
|
||||||
const hasCloudConfigComment = !!key?.commentBefore?.includes('cloud-config');
|
const hasCloudConfigComment = !!firstKey?.commentBefore?.includes('cloud-config');
|
||||||
const isMatchProp = key.source === paths[paths.length - 1];
|
const isFirstProp = firstKey?.source === paths[paths.length - 1];
|
||||||
|
|
||||||
if (key && hasCloudConfigComment && isMatchProp) {
|
if (firstKey && hasCloudConfigComment && isFirstProp) {
|
||||||
// Comments are mounted on the next node and we should not delete the node containing cloud-config
|
const comment = firstKey.commentBefore;
|
||||||
|
|
||||||
|
doc.deleteIn(paths);
|
||||||
|
|
||||||
|
// Move the comment to the new first key; if no keys remain the comment is lost.
|
||||||
|
const newFirstKey = doc.getIn([])?.items?.[0]?.key;
|
||||||
|
|
||||||
|
if (newFirstKey) {
|
||||||
|
newFirstKey.commentBefore = comment;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
doc.deleteIn(paths);
|
doc.deleteIn(paths);
|
||||||
}
|
}
|
||||||
@ -1403,7 +1420,6 @@ export default {
|
|||||||
if (packages.length > 0) {
|
if (packages.length > 0) {
|
||||||
userDataDoc.setIn(['packages'], packages);
|
userDataDoc.setIn(['packages'], packages);
|
||||||
} else {
|
} else {
|
||||||
userDataDoc.setIn(['packages'], []); // It needs to be set empty first, as it is possible that cloud-init comments are mounted on this node
|
|
||||||
this.deleteYamlDocProp(userDataDoc, ['packages']);
|
this.deleteYamlDocProp(userDataDoc, ['packages']);
|
||||||
this.deleteYamlDocProp(userDataDoc, ['package_update']);
|
this.deleteYamlDocProp(userDataDoc, ['package_update']);
|
||||||
}
|
}
|
||||||
@ -1418,7 +1434,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
deleteQGA(config) {
|
deleteQGA(config) {
|
||||||
const { osType, userDataDoc, deletePackage = false } = config;
|
const { osType, userDataDoc } = config;
|
||||||
|
|
||||||
const userDataTemplateValue = this.$store.getters['harvester/byId'](CONFIG_MAP, this.userDataTemplateId)?.data?.cloudInit || '';
|
const userDataTemplateValue = this.$store.getters['harvester/byId'](CONFIG_MAP, this.userDataTemplateId)?.data?.cloudInit || '';
|
||||||
|
|
||||||
@ -1427,6 +1443,15 @@ export default {
|
|||||||
const packages = userDataJSON?.packages || [];
|
const packages = userDataJSON?.packages || [];
|
||||||
const runcmd = userDataJSON?.runcmd || [];
|
const runcmd = userDataJSON?.runcmd || [];
|
||||||
|
|
||||||
|
// Special handling of OS types.
|
||||||
|
let deletePackage = config.deletePackage ?? false;
|
||||||
|
|
||||||
|
switch (osType) {
|
||||||
|
case 'windows':
|
||||||
|
deletePackage = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(packages) && deletePackage) {
|
if (Array.isArray(packages) && deletePackage) {
|
||||||
const templateHasQGAPackage = this.convertToJson(userDataTemplateValue);
|
const templateHasQGAPackage = this.convertToJson(userDataTemplateValue);
|
||||||
|
|
||||||
@ -1452,7 +1477,6 @@ export default {
|
|||||||
if (packages.length > 0) {
|
if (packages.length > 0) {
|
||||||
userDataDoc.setIn(['packages'], packages);
|
userDataDoc.setIn(['packages'], packages);
|
||||||
} else {
|
} else {
|
||||||
userDataDoc.setIn(['packages'], []);
|
|
||||||
this.deleteYamlDocProp(userDataDoc, ['packages']);
|
this.deleteYamlDocProp(userDataDoc, ['packages']);
|
||||||
this.deleteYamlDocProp(userDataDoc, ['package_update']);
|
this.deleteYamlDocProp(userDataDoc, ['package_update']);
|
||||||
}
|
}
|
||||||
@ -1485,7 +1509,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async saveSecret(vm) {
|
async saveSecret(vm) {
|
||||||
if (!vm?.spec || !this.secretName || this.isWindows) {
|
if (!vm?.spec || !this.secretName) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1906,8 +1930,6 @@ export default {
|
|||||||
isWindows(val) {
|
isWindows(val) {
|
||||||
if (val) {
|
if (val) {
|
||||||
this['sshKey'] = [];
|
this['sshKey'] = [];
|
||||||
this['userScript'] = undefined;
|
|
||||||
this['networkScript'] = undefined;
|
|
||||||
this['installAgent'] = false;
|
this['installAgent'] = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1967,9 +1989,8 @@ export default {
|
|||||||
|
|
||||||
osType(neu, old) {
|
osType(neu, old) {
|
||||||
this.installAgent = old === 'windows' ? true : this.installAgent;
|
this.installAgent = old === 'windows' ? true : this.installAgent;
|
||||||
const out = old === 'windows' ? this.getInitUserData({ osType: neu }) : this.getUserData({ installAgent: this.installAgent, osType: neu });
|
this.userScript = this.getUserData({ installAgent: this.installAgent, osType: neu });
|
||||||
|
|
||||||
this['userScript'] = out;
|
|
||||||
this.refreshYamlEditor();
|
this.refreshYamlEditor();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user