Alejandro Bonilla e013534373
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>
2026-07-28 11:32:30 +02:00

166 lines
3.1 KiB
Vue

<script>
import { mapGetters } from 'vuex';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import YamlEditor, { EDITOR_MODES } from '@shell/components/YamlEditor';
import { _VIEW } from '@shell/config/query-params';
import { CONFIG_MAP } from '@shell/config/types';
const _NEW = '_NEW';
const _NONE = '_NONE';
export default {
components: { YamlEditor, LabeledSelect },
emits: ['updateTemplateId', 'show', 'update'],
props: {
mode: {
type: String,
default: 'create'
},
osType: {
type: String,
default: ''
},
value: {
type: String,
default: ''
},
type: {
type: String,
default: ''
},
options: {
type: Array,
default: () => {
return [];
}
},
configId: {
type: String,
default: ''
},
viewCode: {
type: Boolean,
default: false
}
},
data() {
return {
id: '',
yamlScript: this.value,
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
editorMode() {
return this.isView || this.viewCode ? EDITOR_MODES.VIEW_CODE : EDITOR_MODES.EDIT_CODE;
},
isView() {
return this.mode === _VIEW;
},
},
watch: {
value(neu) {
this.yamlScript = neu;
},
configId(neu) {
this.id = this.configId;
},
id(neu, old) {
const cloudInit = this.$store.getters['harvester/byId'](CONFIG_MAP, neu)?.data?.cloudInit || '';
this.$emit('updateTemplateId', this.type, neu);
if (!neu) {
// should not reset yaml when nothing is selected
return;
} else if (neu === _NEW) {
this.$emit('show', this.type);
this.id = old;
return;
} else if (neu === _NONE ) {
this.yamlScript = '';
} else {
this.yamlScript = cloudInit;
}
this.$refs['yaml'].updateValue(cloudInit);
},
yamlScript(neu) {
this.$emit('update', neu, this.type);
}
},
methods: {
refresh() {
this.$refs.yaml.refresh();
},
updateValue() {
this.$refs['yaml'].updateValue(this.value);
}
}
};
</script>
<template>
<div class="mb-20">
<h3>{{ t(`harvester.virtualMachine.cloudConfig.${type}.title`) }}</h3>
<p class="text-muted mb-20">
<t
:k="`harvester.virtualMachine.cloudConfig.${type}.tip`"
:raw="true"
/>
</p>
<LabeledSelect
v-if="!isView"
v-model:value="id"
class="mb-20"
:options="options"
:disabled="viewCode"
:label-key="`harvester.virtualMachine.cloudConfig.${type}.label`"
/>
<div class="resource-yaml">
<YamlEditor
ref="yaml"
v-model:value="yamlScript"
:mode="mode"
class="yaml-editor"
:editor-mode="editorMode"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
$yaml-height: 200px;
:deep() .resource-yaml {
flex: 1;
display: flex;
flex-direction: column;
& .yaml-editor{
flex: 1;
min-height: $yaml-height;
& .code-mirror .CodeMirror {
min-height: $yaml-height;
}
}
}
</style>