mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-08-16 20:59:16 +00:00
feat: Unattend.xml support for Windows (#954)
Documentation: https://kubevirt.io/user-guide/user_workloads/startup_scripts/#sysprep Related to: https://github.com/harvester/harvester/issues/1836 Signed-off-by: Volker Theile <vtheile@suse.com>
This commit is contained in:
parent
479c298f4a
commit
495e80267d
@ -83,4 +83,5 @@ export const HCI = {
|
|||||||
NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map',
|
NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map',
|
||||||
CDI_POPULATOR_KIND: 'cdi.kubevirt.io/storage.populator.kind',
|
CDI_POPULATOR_KIND: 'cdi.kubevirt.io/storage.populator.kind',
|
||||||
CNI_NETWORKS: 'k8s.v1.cni.cncf.io/networks',
|
CNI_NETWORKS: 'k8s.v1.cni.cncf.io/networks',
|
||||||
|
WINDOWS_SYSPREP: 'harvesterhci.io/windows-sysprep',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,346 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import YamlEditor, { EDITOR_MODES } from '@shell/components/YamlEditor';
|
||||||
|
import ModalWithCard from '@shell/components/ModalWithCard';
|
||||||
|
import { SECRET } from '@shell/config/types';
|
||||||
|
|
||||||
|
const _AUTOUNATTENDXML = 'autounattend.xml';
|
||||||
|
const _NEW = '_NEW';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VirtualMachineWindowsSysprep',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Banner,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
ModalWithCard,
|
||||||
|
YamlEditor,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: 'create',
|
||||||
|
},
|
||||||
|
namespace: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
secretName: '',
|
||||||
|
xmlContent: '',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
secretOptions: [],
|
||||||
|
errors: [],
|
||||||
|
secretName: '',
|
||||||
|
xmlContent: '',
|
||||||
|
newSecretName: '',
|
||||||
|
newXmlContent: '',
|
||||||
|
isOpen: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
await this.loadSecretsOptions();
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
// Force the loading of the XML content.
|
||||||
|
this.secretName = this.value?.secretName || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
secretName() {
|
||||||
|
if (this.secretName === _NEW) {
|
||||||
|
this.isOpen = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the content from the given secret, or set empty value.
|
||||||
|
this.xmlContent = this.secretName === '' ? '' : this.loadXmlContentFromSecret(this.secretName);
|
||||||
|
|
||||||
|
// Update the editor form field.
|
||||||
|
this.updateValue();
|
||||||
|
|
||||||
|
// Update the property.
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
xmlContent() {
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
editorMode() {
|
||||||
|
return this.mode === 'view' ? EDITOR_MODES.VIEW_CODE : EDITOR_MODES.EDIT_CODE;
|
||||||
|
},
|
||||||
|
|
||||||
|
isView() {
|
||||||
|
return this.mode === 'view';
|
||||||
|
},
|
||||||
|
|
||||||
|
xmlContentValidationError() {
|
||||||
|
return this.validateXmlContent(this.xmlContent);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
update() {
|
||||||
|
this.$emit('update:value', {
|
||||||
|
secretName: this.secretName,
|
||||||
|
xmlContent: this.xmlContent,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateValue() {
|
||||||
|
this.$refs?.xmlEditor?.updateValue?.(this.xmlContent);
|
||||||
|
},
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.$refs?.xmlEditor?.refresh?.();
|
||||||
|
},
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
if (this.secretName === _NEW) {
|
||||||
|
this.secretName = '';
|
||||||
|
}
|
||||||
|
this.newSecretName = '';
|
||||||
|
this.newXmlContent = '';
|
||||||
|
this.errors = [];
|
||||||
|
this.isOpen = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadSecretsOptions() {
|
||||||
|
const secrets = await this.$store.dispatch('harvester/findAll', { type: SECRET });
|
||||||
|
|
||||||
|
const templates = secrets
|
||||||
|
.filter(
|
||||||
|
(s) => s.metadata?.namespace === this.namespace &&
|
||||||
|
s.decodedData?.[_AUTOUNATTENDXML]
|
||||||
|
)
|
||||||
|
.map((s) => ({
|
||||||
|
label: s.metadata.name,
|
||||||
|
value: `${ s.metadata.namespace }/${ s.metadata.name }`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.secretOptions = [
|
||||||
|
{
|
||||||
|
label: this.t('generic.none'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.t('harvester.virtualMachine.sysprep.createNew'),
|
||||||
|
value: _NEW,
|
||||||
|
},
|
||||||
|
...templates,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(buttonCb) {
|
||||||
|
this.errors = [];
|
||||||
|
|
||||||
|
if (!this.newSecretName?.trim()) {
|
||||||
|
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.name') }, true));
|
||||||
|
buttonCb(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.newXmlContent?.trim()) {
|
||||||
|
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.sysprep.xmlContent') }, true));
|
||||||
|
buttonCb(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const xmlError = this.validateXmlContent(this.newXmlContent);
|
||||||
|
|
||||||
|
if (xmlError) {
|
||||||
|
this.errors.push(xmlError);
|
||||||
|
buttonCb(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const secret = await this.$store.dispatch('harvester/create', {
|
||||||
|
type: SECRET,
|
||||||
|
metadata: {
|
||||||
|
name: this.newSecretName,
|
||||||
|
namespace: this.namespace,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
secret.setData(_AUTOUNATTENDXML, this.newXmlContent);
|
||||||
|
const res = await secret.save();
|
||||||
|
|
||||||
|
if (res?.metadata?.name) {
|
||||||
|
await this.loadSecretsOptions();
|
||||||
|
this.secretName = `${ this.namespace }/${ res.metadata.name }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonCb(true);
|
||||||
|
this.cancel();
|
||||||
|
} catch (err) {
|
||||||
|
this.errors = [err.message];
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
validateXmlContent(content) {
|
||||||
|
if (!content?.trim()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(content, 'text/xml');
|
||||||
|
const parseError = doc.querySelector('parsererror');
|
||||||
|
|
||||||
|
if (parseError) {
|
||||||
|
return this.t('harvester.virtualMachine.sysprep.validation.invalidXml');
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://learn.microsoft.com/en-us/windows-hardware/drivers/gpiobtn/implement-the-unattended-windows-setup-setting
|
||||||
|
if (doc.documentElement.namespaceURI !== 'urn:schemas-microsoft-com:unattend') {
|
||||||
|
return this.t('harvester.virtualMachine.sysprep.validation.invalidNamespace');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
return e.message;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
loadXmlContentFromSecret(id) {
|
||||||
|
const secret = this.$store.getters['harvester/byId'](SECRET, id);
|
||||||
|
|
||||||
|
return secret?.decodedData?.[_AUTOUNATTENDXML] || '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2 v-if="!isView">
|
||||||
|
{{ t('harvester.virtualMachine.sysprep.title') }}
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
v-if="!isView"
|
||||||
|
class="text-muted mb-20"
|
||||||
|
>
|
||||||
|
<t
|
||||||
|
k="harvester.virtualMachine.sysprep.description"
|
||||||
|
:raw="true"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Secret Selection -->
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="secretName"
|
||||||
|
:options="secretOptions"
|
||||||
|
:label="t('harvester.virtualMachine.sysprep.secret.label')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- XML Editor -->
|
||||||
|
<div class="mb-20">
|
||||||
|
<Banner
|
||||||
|
v-if="xmlContentValidationError"
|
||||||
|
color="error"
|
||||||
|
class="mb-10"
|
||||||
|
>
|
||||||
|
<i class="icon icon-error" />
|
||||||
|
{{ xmlContentValidationError }}
|
||||||
|
</Banner>
|
||||||
|
|
||||||
|
<div class="resource-xml">
|
||||||
|
<YamlEditor
|
||||||
|
ref="xmlEditor"
|
||||||
|
v-model:value="xmlContent"
|
||||||
|
:editor-mode="editorMode"
|
||||||
|
class="xml-editor"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ModalWithCard
|
||||||
|
v-if="isOpen"
|
||||||
|
name="createSysprepSecret"
|
||||||
|
width="40%"
|
||||||
|
:errors="errors"
|
||||||
|
@finish="save"
|
||||||
|
@close="cancel"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
{{ t('harvester.virtualMachine.sysprep.createNewTitle') }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #content>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="newSecretName"
|
||||||
|
:label="t('harvester.virtualMachine.input.name')"
|
||||||
|
class="mb-20"
|
||||||
|
required
|
||||||
|
@keydown.native.enter.prevent="()=>{}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="xml">
|
||||||
|
<div class="resource-xml">
|
||||||
|
<YamlEditor
|
||||||
|
ref="createTemplate"
|
||||||
|
v-model:value="newXmlContent"
|
||||||
|
:editor-mode="editorMode"
|
||||||
|
class="xml-editor"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ModalWithCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$xml-height: 350px;
|
||||||
|
|
||||||
|
:deep() .resource-xml {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
& .xml-editor {
|
||||||
|
flex: 1;
|
||||||
|
min-height: $xml-height;
|
||||||
|
font-family: monospace;
|
||||||
|
|
||||||
|
& .code-mirror .CodeMirror {
|
||||||
|
min-height: $xml-height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.xml {
|
||||||
|
height: $xml-height;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -29,6 +29,7 @@ import RestartVMDialog from '../../dialog/RestartVMDialog';
|
|||||||
import PciDevices from './VirtualMachinePciDevices/index';
|
import PciDevices from './VirtualMachinePciDevices/index';
|
||||||
import AccessCredentials from './VirtualMachineAccessCredentials';
|
import AccessCredentials from './VirtualMachineAccessCredentials';
|
||||||
import CloudConfig from './VirtualMachineCloudConfig';
|
import CloudConfig from './VirtualMachineCloudConfig';
|
||||||
|
import WindowsSysprep from './VirtualMachineWindowsSysprep';
|
||||||
import CpuMemory from './VirtualMachineCpuMemory';
|
import CpuMemory from './VirtualMachineCpuMemory';
|
||||||
import CpuModel from './VirtualMachineCpuModel';
|
import CpuModel from './VirtualMachineCpuModel';
|
||||||
import Network from './VirtualMachineNetwork';
|
import Network from './VirtualMachineNetwork';
|
||||||
@ -59,6 +60,7 @@ export default {
|
|||||||
CpuMemory,
|
CpuMemory,
|
||||||
CpuModel,
|
CpuModel,
|
||||||
CloudConfig,
|
CloudConfig,
|
||||||
|
WindowsSysprep,
|
||||||
NodeScheduling,
|
NodeScheduling,
|
||||||
PodAffinity,
|
PodAffinity,
|
||||||
AccessCredentials,
|
AccessCredentials,
|
||||||
@ -324,6 +326,10 @@ export default {
|
|||||||
try {
|
try {
|
||||||
await this.saveSecret(res);
|
await this.saveSecret(res);
|
||||||
await this.saveAccessCredentials(res);
|
await this.saveAccessCredentials(res);
|
||||||
|
|
||||||
|
if (this.isWindows) {
|
||||||
|
await this.saveSysprepConfig(res);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errors.push(...exceptionToErrorsArray(e));
|
this.errors.push(...exceptionToErrorsArray(e));
|
||||||
}
|
}
|
||||||
@ -369,6 +375,7 @@ export default {
|
|||||||
clear(this.errors);
|
clear(this.errors);
|
||||||
|
|
||||||
this.validateCPUMemory();
|
this.validateCPUMemory();
|
||||||
|
this.validateWindowsSysprep();
|
||||||
|
|
||||||
// block create VM flow if has validation errors
|
// block create VM flow if has validation errors
|
||||||
if (this.errors.length) {
|
if (this.errors.length) {
|
||||||
@ -396,6 +403,24 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
validateWindowsSysprep() {
|
||||||
|
if (!this.isWindows) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.sysprep?.secretName?.trim?.() && !this.sysprep?.xmlContent?.trim?.()) {
|
||||||
|
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.sysprep.xmlContent') }, true));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sysprepValidationError = this.$refs.sysprepConfig?.xmlContentValidationError;
|
||||||
|
|
||||||
|
if (sysprepValidationError) {
|
||||||
|
this.errors.push(sysprepValidationError);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async saveSingle(buttonCb) {
|
async saveSingle(buttonCb) {
|
||||||
this.parseVM();
|
this.parseVM();
|
||||||
this.value.spec.template.spec.hostname = this.hostname ? this.hostname : this.value.metadata.name;
|
this.value.spec.template.spec.hostname = this.hostname ? this.hostname : this.value.metadata.name;
|
||||||
@ -546,6 +571,7 @@ export default {
|
|||||||
onTabChanged({ tab }) {
|
onTabChanged({ tab }) {
|
||||||
if (tab.name === 'advanced') {
|
if (tab.name === 'advanced') {
|
||||||
this.$refs.yamlEditor?.refresh();
|
this.$refs.yamlEditor?.refresh();
|
||||||
|
this.$refs.sysprepConfig?.refresh();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -1019,11 +1045,11 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CloudConfig
|
<CloudConfig
|
||||||
|
v-if="!isWindows"
|
||||||
ref="yamlEditor"
|
ref="yamlEditor"
|
||||||
:user-script="userScript"
|
:user-script="userScript"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:os-type="osType"
|
:os-type="osType"
|
||||||
:view-code="isWindows"
|
|
||||||
:namespace="value.metadata.namespace"
|
:namespace="value.metadata.namespace"
|
||||||
:network-script="networkScript"
|
:network-script="networkScript"
|
||||||
@updateUserData="updateUserData"
|
@updateUserData="updateUserData"
|
||||||
@ -1031,6 +1057,14 @@ export default {
|
|||||||
@updateDataTemplateId="updateDataTemplateId"
|
@updateDataTemplateId="updateDataTemplateId"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<WindowsSysprep
|
||||||
|
v-if="isWindows"
|
||||||
|
ref="sysprepConfig"
|
||||||
|
v-model:value="sysprep"
|
||||||
|
:mode="mode"
|
||||||
|
:namespace="value.metadata.namespace"
|
||||||
|
/>
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-if="value.cpuPinningFeatureEnabled"
|
v-if="value.cpuPinningFeatureEnabled"
|
||||||
v-model:value="cpuPinning"
|
v-model:value="cpuPinning"
|
||||||
|
|||||||
@ -7,6 +7,7 @@ generic:
|
|||||||
inProgress: In Progress
|
inProgress: In Progress
|
||||||
basic: Basic
|
basic: Basic
|
||||||
loading: Loading...
|
loading: Loading...
|
||||||
|
none: None
|
||||||
|
|
||||||
unsupported:
|
unsupported:
|
||||||
serverVersion: 'Current version: <code>{serverVersion}</code>'
|
serverVersion: 'Current version: <code>{serverVersion}</code>'
|
||||||
@ -797,6 +798,17 @@ harvester:
|
|||||||
label: Network Data Template
|
label: Network Data Template
|
||||||
title: "Network Data:"
|
title: "Network Data:"
|
||||||
tip: "The network-data configuration allows you to customize the instance's networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>"
|
tip: "The network-data configuration allows you to customize the instance's networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>"
|
||||||
|
sysprep:
|
||||||
|
title: Windows Sysprep Configuration
|
||||||
|
description: "Configure Windows automated installation using autounattend.xml. The configuration will be stored in a Kubernetes Secret and mounted as a CD-ROM during installation. <a href='https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/automate-windows-setup' target='_blank'>Learn more</a>"
|
||||||
|
createNew: Create new...
|
||||||
|
createNewTitle: Create Windows Sysprep Template
|
||||||
|
xmlContent: autounattend.xml content
|
||||||
|
secret:
|
||||||
|
label: Windows Sysprep Template
|
||||||
|
validation:
|
||||||
|
invalidXml: Invalid XML syntax
|
||||||
|
invalidNamespace: Invalid unattend.xml namespace. Expected 'urn:schemas-microsoft-com:unattend'
|
||||||
scheduling:
|
scheduling:
|
||||||
affinity:
|
affinity:
|
||||||
anyNode: 'Run virtual machine on any available node'
|
anyNode: 'Run virtual machine on any available node'
|
||||||
|
|||||||
@ -255,6 +255,29 @@ export default {
|
|||||||
return this.convertToJson(userData)?.ssh_authorized_keys || [];
|
return this.convertToJson(userData)?.ssh_authorized_keys || [];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getSysprepConfig(spec) {
|
||||||
|
const sysprepVolume = spec?.template?.spec?.volumes?.find(
|
||||||
|
(v) => v.name === 'sysprep' && v.sysprep?.secret
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sysprepVolume) {
|
||||||
|
return { secretName: '', xmlContent: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const namespace = this.value.metadata.namespace;
|
||||||
|
const secretName = sysprepVolume.sysprep.secret.name;
|
||||||
|
const secret = this.$store.getters[`${ inStore }/byId`](
|
||||||
|
SECRET,
|
||||||
|
`${ namespace }/${ secretName }`
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
secretName: `${ namespace }/${ secretName }`,
|
||||||
|
xmlContent: secret?.decodedData?.['autounattend.xml'] || ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
compareSSHValue(a = '', b = '') {
|
compareSSHValue(a = '', b = '') {
|
||||||
const r = /(\r\n\t|\n|\r\t)|(\s*)/gm;
|
const r = /(\r\n\t|\n|\r\t)|(\s*)/gm;
|
||||||
|
|
||||||
|
|||||||
@ -186,6 +186,7 @@ export default {
|
|||||||
terminationGracePeriodSeconds: '',
|
terminationGracePeriodSeconds: '',
|
||||||
cpuPinning: false,
|
cpuPinning: false,
|
||||||
cpuModel: '',
|
cpuModel: '',
|
||||||
|
sysprep: { secretName: '', xmlContent: '' },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -445,6 +446,17 @@ export default {
|
|||||||
this['diskRows'] = diskRows;
|
this['diskRows'] = diskRows;
|
||||||
this['filesystemRows'] = this.getFilesystemRows(vm);
|
this['filesystemRows'] = this.getFilesystemRows(vm);
|
||||||
|
|
||||||
|
let sysprepConfig = { secretName: '', xmlContent: '' };
|
||||||
|
|
||||||
|
if (osType === 'windows') {
|
||||||
|
sysprepConfig = this.getSysprepConfig(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
this['sysprep'] = {
|
||||||
|
secretName: sysprepConfig.secretName || this.sysprep?.secretName || '',
|
||||||
|
xmlContent: sysprepConfig.xmlContent || this.sysprep?.xmlContent || '',
|
||||||
|
};
|
||||||
|
|
||||||
this.refreshYamlEditor();
|
this.refreshYamlEditor();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -609,7 +621,8 @@ export default {
|
|||||||
|
|
||||||
out = sortBy(out, 'bootOrder');
|
out = sortBy(out, 'bootOrder');
|
||||||
|
|
||||||
return out.filter( (O) => O.name !== 'cloudinitdisk');
|
// Filter out cloudinitdisk and sysprep disk from UI display.
|
||||||
|
return out.filter( (O) => O.name !== 'cloudinitdisk' && O.name !== 'sysprep');
|
||||||
},
|
},
|
||||||
|
|
||||||
getNetworkRows(vm, config) {
|
getNetworkRows(vm, config) {
|
||||||
@ -828,6 +841,32 @@ export default {
|
|||||||
this.secretName = this.generateSecretName(this.secretNamePrefix);
|
this.secretName = this.generateSecretName(this.secretNamePrefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!disks.find((D) => D.name === 'sysprep') && this.isWindows) {
|
||||||
|
const hasSysprepContent = !!this.sysprep.xmlContent?.trim?.();
|
||||||
|
|
||||||
|
// If we have content but no secret name, it's a new secret that needs a name.
|
||||||
|
if (hasSysprepContent && !this.sysprep.secretName?.trim?.()) {
|
||||||
|
const prefix = this.secretNamePrefix ? `${ this.secretNamePrefix }-windows-sysprep` : 'windows-sysprep';
|
||||||
|
|
||||||
|
this.sysprep.secretName = `${ this.value.metadata.namespace }/${ this.generateSecretName(prefix) }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve/attach sysprep whenever a secret is selected/known.
|
||||||
|
if (this.sysprep.secretName) {
|
||||||
|
disks.push({
|
||||||
|
name: 'sysprep',
|
||||||
|
cdrom: { bus: 'sata' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretName = this.sysprep.secretName.split('/')[1] || this.sysprep.secretName;
|
||||||
|
|
||||||
|
volumes.push({
|
||||||
|
name: 'sysprep',
|
||||||
|
sysprep: { secret: { name: secretName } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!disks.find( (D) => D.name === 'cloudinitdisk') && (this.userData || this.networkData)) {
|
if (!disks.find( (D) => D.name === 'cloudinitdisk') && (this.userData || this.networkData)) {
|
||||||
if (!this.isWindows) {
|
if (!this.isWindows) {
|
||||||
disks.push({
|
disks.push({
|
||||||
@ -836,7 +875,6 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const userData = this.getUserData({ osType: this.osType, installAgent: this.installAgent });
|
const userData = this.getUserData({ osType: this.osType, installAgent: this.installAgent });
|
||||||
|
|
||||||
const cloudinitdisk = {
|
const cloudinitdisk = {
|
||||||
name: 'cloudinitdisk',
|
name: 'cloudinitdisk',
|
||||||
cloudInitNoCloud: {}
|
cloudInitNoCloud: {}
|
||||||
@ -1482,6 +1520,42 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async saveSysprepConfig(vm) {
|
||||||
|
if (!this.isWindows) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.sysprep.secretName?.trim?.() && !this.sysprep.xmlContent?.trim?.()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretName = this.sysprep.secretName.split('/')[1] || this.sysprep.secretName;
|
||||||
|
const namespace = vm.metadata.namespace;
|
||||||
|
const namespacedName = `${ namespace }/${ secretName }`;
|
||||||
|
|
||||||
|
let secret;
|
||||||
|
|
||||||
|
try {
|
||||||
|
secret = await this.$store.dispatch('harvester/find', { type: SECRET, id: namespacedName });
|
||||||
|
} catch (e) {
|
||||||
|
if (e?.status !== 404) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
secret = await this.$store.dispatch('harvester/create', {
|
||||||
|
type: SECRET,
|
||||||
|
metadata: {
|
||||||
|
name: secretName,
|
||||||
|
namespace,
|
||||||
|
labels: { [HCI_ANNOTATIONS.WINDOWS_SYSPREP]: 'true' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
secret.setData('autounattend.xml', this.sysprep.xmlContent);
|
||||||
|
await secret.save();
|
||||||
|
},
|
||||||
|
|
||||||
getAccessCredentialsValidation() {
|
getAccessCredentialsValidation() {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user