mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-11-16 05:46:14 +08:00
* perf: speed up the VM list with many VMs The VM list scanned whole store lists for every row and re-evaluated the same values per row on every render and sort. With several hundred VMs this blocked the browser on sorting and on every VM update. Performance: - look up launcher pods and migrations through shared indexes (namespace/name -> resource) instead of scanning per row; the indexes are computed per store list, so they stay in sync with in-place store updates - use the same migration lookup (getLatestMigration) in the VM model and on the VM detail page - look up PVCs, PVs, VMIs and restore backups by id - merge VMs and VMIs with a Set - evaluate the VM state once when sorting by state - check the unschedulable condition before the pod based state checks - cache feature flag results and per-schema/per-setting values that are the same for every row Bug fixes: - the launcher pod map was never rebuilt, because the store keeps the same array - pods, volumes and migrations of a VM with the same name in another namespace could be picked up The order of the volumes of a VM (vm.volumes) now follows the VM spec instead of the store order. Nothing depends on it. Measured in a synthetic setup, 100 rows per page, sorting by state (Chrome, dev build): - ~490 VMs, ~500 PVCs/PVs: 1.9 s -> 1.0 s - ~490 VMs, 600 PVs, 200 migrations: 6.0 s -> 0.7 s, main thread blocked during VM updates 78% -> 32% With few volumes the gain is much smaller (8-20%). Related to: https://github.com/harvester/harvester/issues/9513 Signed-off-by: Volker Theile <vtheile@suse.com> * fix: read VM data from the store of the resource instead of a fixed one The migrations of a VMI and the migration badge of the state column were read from the hardcoded `harvester` store. In the Rancher explorer (embedded and standalone) the data is loaded into the `cluster` store, so a running migration was shown neither in the VM state nor as badge. The VM model remembered the store of the current product in a module variable on first use and never updated it. The same model is used in the Harvester product (`harvester` store) and in the Rancher explorer (`cluster` store), and Rancher keeps the page loaded when going from the explorer of a Harvester cluster to Virtualization Management and into the Harvester product. The VM list then read the VMIs, launcher pods, volumes and restores from the wrong store, and running VMs were shown as Starting until the page was reloaded. Read the data from the store the resource lives in (`this.$getters`, in the formatter the store of the current product). In the Harvester product this is still the `harvester` store, so nothing changes there. Not covered: volume details like the lock icon of encrypted volumes. The explorer uses the generic Rancher PVC model, which does not know them, and settings based checks (feature flags, backup target) are not loaded there either. Related to: https://github.com/harvester/harvester/issues/11882 Signed-off-by: Volker Theile <vtheile@suse.com> --------- Signed-off-by: Volker Theile <vtheile@suse.com>
1393 lines
36 KiB
JavaScript
1393 lines
36 KiB
JavaScript
import { load } from 'js-yaml';
|
|
import { omitBy, pickBy } from 'lodash';
|
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
|
import { colorForState, stateSort } from '@shell/plugins/dashboard-store/resource-class';
|
|
import { POD, NODE, PVC } from '@shell/config/types';
|
|
import { findBy } from '@shell/utils/array';
|
|
import { parseSi } from '@shell/utils/units';
|
|
import { get, set } from '@shell/utils/object';
|
|
import { LABELS_TO_IGNORE_REGEX, HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
|
import { _CLONE } from '@shell/config/query-params';
|
|
import { matchesSomeRegex } from '@shell/utils/string';
|
|
import { parseVolumeClaimTemplates } from '@pkg/utils/vm';
|
|
import { BACKUP_TYPE } from '../config/types';
|
|
import { HCI } from '../types';
|
|
import HarvesterResource from './harvester';
|
|
import { getVmCPUMemoryValues } from '../utils/cpuMemory';
|
|
import { sharedComputed } from '../utils/shared-computed';
|
|
import { isBackupTargetSettingUnavailable } from '../utils/setting';
|
|
|
|
export const OFF = 'Off';
|
|
|
|
const VMI_WAITING_MESSAGE =
|
|
'The virtual machine is waiting for resources to become available.';
|
|
const VM_ERROR = 'VM error';
|
|
const STOPPING = 'Stopping';
|
|
const UNSCHEDULABLE = 'Unschedulable';
|
|
const WAITING = 'Waiting';
|
|
const NOT_READY = 'Not Ready';
|
|
const AGENT_CONNECTED = 'AgentConnected';
|
|
|
|
const PAUSED = 'Paused';
|
|
const PAUSED_VM_MODAL_MESSAGE =
|
|
'This VM has been paused. If you wish to unpause it, please click the Unpause button below. For further details, please check with your system administrator.';
|
|
|
|
const POD_STATUS_NOT_SCHEDULABLE = 'POD_NOT_SCHEDULABLE';
|
|
const POD_STATUS_CONTAINER_FAILING = 'POD_CONTAINER_FAILING';
|
|
|
|
const POD_STATUS_FAILED = 'POD_FAILED';
|
|
const POD_STATUS_CRASHLOOP_BACKOFF = 'POD_CRASHLOOP_BACKOFF';
|
|
const POD_STATUS_UNKNOWN = 'POD_STATUS_UNKNOWN';
|
|
|
|
const POD_STATUS_ALL_ERROR = [
|
|
POD_STATUS_NOT_SCHEDULABLE,
|
|
POD_STATUS_CONTAINER_FAILING,
|
|
POD_STATUS_FAILED,
|
|
POD_STATUS_CRASHLOOP_BACKOFF,
|
|
POD_STATUS_UNKNOWN
|
|
];
|
|
|
|
const POD_STATUS_COMPLETED = 'POD_STATUS_COMPLETED';
|
|
const POD_STATUS_SUCCEEDED = 'POD_STATUS_SUCCEEDED';
|
|
const POD_STATUS_RUNNING = 'POD_STATUS_RUNNING';
|
|
|
|
const POD_STATUS_ALL_READY = [
|
|
POD_STATUS_RUNNING,
|
|
POD_STATUS_COMPLETED,
|
|
POD_STATUS_SUCCEEDED
|
|
];
|
|
|
|
const RunStrategy = {
|
|
Always: 'Always',
|
|
RerunOnFailure: 'RerunOnFailure',
|
|
Halted: 'Halted',
|
|
Manual: 'Manual'
|
|
};
|
|
|
|
const StateChangeRequest = {
|
|
Start: 'Start',
|
|
Stop: 'Stop'
|
|
};
|
|
|
|
const STARTING_MESSAGE =
|
|
'This virtual machine will start shortly. Preparing storage, networking, and compute resources.';
|
|
|
|
const VMIPhase = {
|
|
Pending: 'Pending',
|
|
Scheduling: 'Scheduling',
|
|
Scheduled: 'Scheduled',
|
|
Running: 'Running',
|
|
Succeeded: 'Succeeded',
|
|
Failed: 'Failed',
|
|
Unknown: 'Unknown'
|
|
};
|
|
|
|
// Index of the pods (namespace/ownerName -> pod), shared by all rows. `podList` must be the store list, not a copy.
|
|
// If several pods have the same owner (e.g. while a VM migrates), the last one wins.
|
|
function getPodOwnerIndex(podList) {
|
|
return sharedComputed(podList, 'podOwnerIndex', (pods) => {
|
|
const map = new Map(); // namespace/ownerName -> pod
|
|
|
|
for (const pod of pods) {
|
|
const refName = pod.metadata?.ownerReferences?.[0]?.name;
|
|
|
|
if (refName) {
|
|
map.set(`${ pod.metadata.namespace }/${ refName }`, pod);
|
|
}
|
|
}
|
|
|
|
return map;
|
|
});
|
|
}
|
|
|
|
// `getters` are the getters of the store of the VM (`this.$getters`)
|
|
function getPodByOwnerName(getters, namespace, ownerName) {
|
|
const podList = getters['all'](POD);
|
|
|
|
if (!Array.isArray(podList)) {
|
|
return undefined;
|
|
}
|
|
|
|
return getPodOwnerIndex(podList).get(`${ namespace }/${ ownerName }`);
|
|
}
|
|
|
|
// A VM only uses PVCs of its own namespace, so look them up by id instead of scanning all PVCs
|
|
function getPvcsByNames(getters, namespace, names) {
|
|
const out = [];
|
|
|
|
for (const name of new Set(names)) {
|
|
const pvc = getters['byId'](PVC, `${ namespace }/${ name }`);
|
|
|
|
if (pvc) {
|
|
out.push(pvc);
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
const IgnoreMessages = ['pod has unbound immediate PersistentVolumeClaims'];
|
|
|
|
export default class VirtVm extends HarvesterResource {
|
|
get availableActions() {
|
|
let out = super._availableActions;
|
|
|
|
if (this.isCloneBackendStorageCloning || this.isCloneBackendStorageFailed) {
|
|
out = out.filter(({ action }) => action !== 'goToClone');
|
|
}
|
|
|
|
const clone = out.find((action) => action.action === 'goToClone');
|
|
|
|
if (clone) {
|
|
clone.action = 'goToCloneVM';
|
|
}
|
|
|
|
const scheduleSchema = this.$getters?.['schemaFor']?.(HCI.SCHEDULE_VM_BACKUP);
|
|
const canCreateVMSSchedule = !!scheduleSchema && sharedComputed(scheduleSchema, 'canPost', (schema) => !!schema.collectionMethods?.find((x) => ['post'].includes(x.toLowerCase())));
|
|
|
|
return [
|
|
{
|
|
action: 'stopVM',
|
|
altAction: 'altStopVM',
|
|
enabled: !!this.actions?.stop,
|
|
icon: 'icon icon-close',
|
|
label: this.t('harvester.action.stop'),
|
|
bulkable: true,
|
|
bulkAction: 'stopVM',
|
|
},
|
|
{
|
|
action: 'forceStop',
|
|
enabled: !!this.actions?.forceStop,
|
|
icon: 'icon icon-close',
|
|
label: this.t('harvester.action.forceStop'),
|
|
bulkable: true
|
|
},
|
|
{
|
|
action: 'pauseVM',
|
|
altAction: 'altPauseVM',
|
|
enabled: !!this.actions?.pause,
|
|
icon: 'icon icon-pause',
|
|
label: this.t('harvester.action.pause')
|
|
},
|
|
{
|
|
action: 'unpauseVM',
|
|
enabled: !!this.actions?.unpause,
|
|
icon: 'icon icon-spinner',
|
|
label: this.t('harvester.action.unpause')
|
|
},
|
|
{
|
|
action: 'restartVM',
|
|
altAction: 'altRestartVM',
|
|
enabled: !!this.actions?.restart,
|
|
icon: 'icon icon-refresh',
|
|
label: this.t('harvester.action.restart'),
|
|
bulkable: true,
|
|
bulkAction: 'restartVM'
|
|
},
|
|
{
|
|
action: 'softrebootVM',
|
|
altAction: 'doSoftReboot',
|
|
enabled: !!this.actions?.softreboot,
|
|
icon: 'icon icon-pipeline',
|
|
label: this.t('harvester.action.softreboot')
|
|
},
|
|
{
|
|
action: 'startVM',
|
|
enabled: !!this.actions?.start,
|
|
icon: 'icon icon-play',
|
|
label: this.t('harvester.action.start'),
|
|
bulkable: true,
|
|
bulkAction: 'startVM'
|
|
},
|
|
{
|
|
action: 'backupVM',
|
|
enabled: !!this.actions?.backup && !this.isBackupTargetUnavailable,
|
|
icon: 'icon icon-backup',
|
|
label: this.t('harvester.action.backup')
|
|
},
|
|
{
|
|
action: 'takeVMSnapshot',
|
|
enabled: (!!this.actions?.snapshot || !!this.actions?.backup),
|
|
icon: 'icon icon-snapshot',
|
|
label: this.t('harvester.action.vmSnapshot')
|
|
},
|
|
{
|
|
action: 'editVMQuota',
|
|
enabled: !!this.actions?.updateResourceQuota && !!this.actions.deleteResourceQuota,
|
|
icon: 'icon icon-storage',
|
|
label: this.t('harvester.action.editVMQuota')
|
|
},
|
|
{
|
|
action: 'cpuMemoryHotplug',
|
|
enabled: !!this.actions?.cpuAndMemoryHotplug,
|
|
icon: 'icon icon-os-management',
|
|
label: this.t('harvester.action.cpuAndMemoryHotplug')
|
|
},
|
|
{
|
|
action: 'createSchedule',
|
|
enabled: canCreateVMSSchedule && this.schedulingVMBackupFeatureEnabled,
|
|
icon: 'icon icon-history',
|
|
label: this.t('harvester.action.createSchedule')
|
|
},
|
|
{
|
|
action: 'restoreVM',
|
|
enabled: !!this.actions?.restore,
|
|
icon: 'icon icon-backup-restore',
|
|
label: this.t('harvester.action.restore')
|
|
},
|
|
{
|
|
action: 'ejectCDROM',
|
|
enabled: !this.hotplugCdRomEnabled && !!this.actions?.ejectCdRom,
|
|
icon: 'icon icon-delete',
|
|
label: this.t('harvester.action.ejectCDROM')
|
|
},
|
|
{
|
|
action: 'migrateVM',
|
|
enabled: !!this.actions?.migrate,
|
|
icon: 'icon icon-copy',
|
|
label: this.t('harvester.action.vmMigrate'),
|
|
bulkable: true,
|
|
bulkAction: 'migrateVM'
|
|
},
|
|
{
|
|
action: 'abortMigrationVM',
|
|
enabled: !!this.actions?.abortMigration,
|
|
icon: 'icon icon-close',
|
|
label: this.t('harvester.action.abortMigration')
|
|
},
|
|
{
|
|
action: 'storageMigration',
|
|
enabled: !!this.actions?.storageMigration,
|
|
icon: 'icon icon-copy',
|
|
label: this.t('harvester.action.storageMigration')
|
|
},
|
|
{
|
|
action: 'cancelStorageMigration',
|
|
enabled: !!this.actions?.cancelStorageMigration,
|
|
icon: 'icon icon-close',
|
|
label: this.t('harvester.action.cancelStorageMigration')
|
|
},
|
|
{
|
|
action: 'addHotplugVolume',
|
|
enabled: !!this.actions?.addVolume,
|
|
icon: 'icon icon-plus',
|
|
label: this.t('harvester.action.addHotplugVolume')
|
|
},
|
|
{
|
|
action: 'addHotplugNic',
|
|
enabled: this.hotplugNicFeatureEnabled && !!this.actions?.addNic,
|
|
icon: 'icon icon-plus',
|
|
label: this.t('harvester.action.addHotplugNic')
|
|
},
|
|
{
|
|
action: 'createTemplate',
|
|
enabled: !!this.actions?.createTemplate,
|
|
icon: 'icon icon-copy',
|
|
label: this.t('harvester.action.createTemplate')
|
|
},
|
|
{
|
|
action: 'openLogs',
|
|
enabled: !!this.podResource,
|
|
icon: 'icon icon-fw icon-chevron-right',
|
|
label: this.t('harvester.action.viewlogs'),
|
|
total: 1
|
|
},
|
|
...out
|
|
];
|
|
}
|
|
|
|
applyDefaults(resources = this, realMode) {
|
|
const spec = {
|
|
runStrategy: 'RerunOnFailure',
|
|
template: {
|
|
metadata: { annotations: {}, labels: {} },
|
|
spec: {
|
|
domain: {
|
|
machine: { type: '' },
|
|
cpu: {
|
|
cores: null,
|
|
sockets: 1,
|
|
threads: 1
|
|
},
|
|
devices: {
|
|
inputs: [
|
|
{
|
|
bus: 'usb',
|
|
name: 'tablet',
|
|
type: 'tablet'
|
|
}
|
|
],
|
|
interfaces: [
|
|
{
|
|
masquerade: {},
|
|
model: 'virtio',
|
|
name: 'default'
|
|
}
|
|
],
|
|
disks: []
|
|
},
|
|
resources: {
|
|
limits: {
|
|
memory: null,
|
|
cpu: ''
|
|
}
|
|
},
|
|
features: { acpi: { enabled: true } }
|
|
},
|
|
evictionStrategy: 'LiveMigrateIfPossible',
|
|
hostname: '',
|
|
networks: [
|
|
{
|
|
name: 'default',
|
|
pod: {}
|
|
}
|
|
],
|
|
volumes: [],
|
|
affinity: {},
|
|
}
|
|
}
|
|
};
|
|
|
|
if (realMode !== _CLONE) {
|
|
this.metadata['annotations'] = { [HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE]: '[]' };
|
|
this.metadata['labels'] = {};
|
|
this['spec'] = spec;
|
|
}
|
|
}
|
|
|
|
cleanForNew() {
|
|
this.$dispatch(`cleanForNew`, this);
|
|
|
|
this.spec.template.spec.hostname = '';
|
|
const interfaces = this.spec.template.spec.domain.devices?.interfaces || [];
|
|
|
|
for (let i = 0; i < interfaces.length; i++) {
|
|
if (interfaces[i].macAddress) {
|
|
interfaces[i].macAddress = '';
|
|
}
|
|
}
|
|
|
|
// delete, spec?.dataSource: The original data should not be saved when clone template
|
|
const deleteDataSource = this.volumeClaimTemplates.map((volume) => {
|
|
if (volume?.spec?.dataSource) {
|
|
delete volume.spec.dataSource;
|
|
}
|
|
|
|
return volume;
|
|
});
|
|
|
|
this.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
|
}
|
|
|
|
restartVM(resources = this) {
|
|
this.$dispatch('promptModal', {
|
|
resources,
|
|
action: 'restart',
|
|
warningMessageKey: 'dialog.confirmExecution.restart.message',
|
|
component: 'ConfirmExecutionDialog'
|
|
});
|
|
}
|
|
|
|
doSoftReboot() {
|
|
this.doActionGrowl('softreboot', {});
|
|
}
|
|
|
|
softrebootVM(resources = this) {
|
|
this.$dispatch('promptModal', {
|
|
resources,
|
|
action: 'softreboot',
|
|
warningMessageKey: 'dialog.confirmExecution.softreboot.message',
|
|
component: 'ConfirmExecutionDialog'
|
|
});
|
|
}
|
|
|
|
openLogs() {
|
|
this.$dispatch(
|
|
'wm/open',
|
|
{
|
|
id: `${ this.id }-logs`,
|
|
label: this.nameDisplay,
|
|
icon: 'file',
|
|
component: 'ContainerLogs',
|
|
attrs: {
|
|
pod: this.podResource,
|
|
initialContainer: this.podResource.metadata.annotations['kubectl.kubernetes.io/default-container']
|
|
}
|
|
},
|
|
{ root: true }
|
|
);
|
|
}
|
|
|
|
createSchedule(resources = this) {
|
|
const router = this.currentRouter();
|
|
|
|
router.push({
|
|
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
|
|
params: { resource: HCI.SCHEDULE_VM_BACKUP },
|
|
query: { vmNamespace: this.metadata.namespace, vmName: this.metadata.name }
|
|
});
|
|
}
|
|
|
|
storageMigration(resources = this) {
|
|
this.$dispatch('promptModal', {
|
|
resources,
|
|
component: 'HarvesterStorageMigrationDialog'
|
|
});
|
|
}
|
|
|
|
backupVM(resources = this) {
|
|
this.$dispatch('promptModal', {
|
|
resources,
|
|
component: 'HarvesterBackupModal'
|
|
});
|
|
}
|
|
|
|
takeVMSnapshot(resources = this) {
|
|
this.$dispatch('promptModal', {
|
|
resources,
|
|
component: 'HarvesterVMSnapshotDialog'
|
|
});
|
|
}
|
|
|
|
editVMQuota(resources = this) {
|
|
this.$dispatch('promptModal', {
|
|
resources,
|
|
snapshotSizeQuota: this.snapshotSizeQuota,
|
|
component: 'HarvesterQuotaDialog'
|
|
});
|
|
}
|
|
|
|
unplugVolume(diskName) {
|
|
const resources = this;
|
|
|
|
this.$dispatch('promptModal', {
|
|
resources,
|
|
name: diskName,
|
|
type: 'volume',
|
|
component: 'HarvesterHotUnplug',
|
|
});
|
|
}
|
|
|
|
ejectCdRomVolume(diskName) {
|
|
const resources = this;
|
|
|
|
this.$dispatch('promptModal', {
|
|
resources,
|
|
name: diskName,
|
|
type: 'cdrom',
|
|
component: 'HarvesterHotUnplug',
|
|
});
|
|
}
|
|
|
|
unplugNIC(networkName) {
|
|
const resources = this;
|
|
|
|
this.$dispatch('promptModal', {
|
|
resources,
|
|
name: networkName,
|
|
type: 'network',
|
|
component: 'HarvesterHotUnplug',
|
|
});
|
|
}
|
|
|
|
restoreVM(resources = this) {
|
|
this.$dispatch('promptModal', {
|
|
resources,
|
|
component: 'HarvesterRestoreDialog'
|
|
});
|
|
}
|
|
|
|
get machineType() {
|
|
return this.spec?.template?.spec?.domain?.machine?.type || '';
|
|
}
|
|
|
|
get realAttachNodeName() {
|
|
const vmi = this.$getters['byId'](HCI.VMI, this.id);
|
|
const nodeName = vmi?.status?.nodeName;
|
|
const node = this.$getters['byId'](NODE, nodeName);
|
|
|
|
return node?.nameDisplay || '';
|
|
}
|
|
|
|
get nodeName() {
|
|
const vmi = this.$getters['byId'](HCI.VMI, this.id);
|
|
const nodeName = vmi?.status?.nodeName;
|
|
const node = this.$getters['byId'](NODE, nodeName);
|
|
|
|
return node?.id;
|
|
}
|
|
|
|
pauseVM(resources = this) {
|
|
this.$dispatch('promptModal', {
|
|
resources,
|
|
action: 'pause',
|
|
warningMessageKey: 'dialog.confirmExecution.pause.message',
|
|
component: 'ConfirmExecutionDialog'
|
|
});
|
|
}
|
|
|
|
altPauseVM() {
|
|
this.doActionGrowl('pause', {});
|
|
}
|
|
|
|
goToCloneVM(resources = this) {
|
|
this.$dispatch('promptModal', {
|
|
resources,
|
|
component: 'CloneVmDialog'
|
|
});
|
|
}
|
|
|
|
unpauseVM() {
|
|
this.doActionGrowl('unpause', {});
|
|
}
|
|
|
|
stopVM(resources = this) {
|
|
this.$dispatch('promptModal', {
|
|
resources,
|
|
action: 'stop',
|
|
warningMessageKey: 'dialog.confirmExecution.stop.message',
|
|
component: 'ConfirmExecutionDialog'
|
|
});
|
|
}
|
|
|
|
async altRestartVM() {
|
|
await this.doActionGrowl('restart', {});
|
|
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
|
|
}
|
|
|
|
async altStopVM() {
|
|
await this.doActionGrowl('stop', {});
|
|
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
|
|
}
|
|
|
|
async forceStop() {
|
|
await this.doActionGrowl('forceStop', {});
|
|
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
|
|
}
|
|
|
|
async startVM(resources = this) {
|
|
const list = Array.isArray(resources) ? resources : [resources];
|
|
|
|
for (const r of list) {
|
|
await r.doActionGrowl('start', {});
|
|
}
|
|
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
|
|
}
|
|
|
|
async download() {
|
|
await super.download();
|
|
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
|
|
}
|
|
|
|
async downloadBulk(items) {
|
|
await super.downloadBulk(items);
|
|
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
|
|
}
|
|
|
|
migrateVM(resources = this) {
|
|
this.$dispatch('promptModal', {
|
|
resources,
|
|
component: 'HarvesterMigrationDialog'
|
|
});
|
|
}
|
|
|
|
ejectCDROM(resources = this) {
|
|
this.$dispatch('promptModal', {
|
|
resources,
|
|
component: 'HarvesterEjectCDROMDialog'
|
|
});
|
|
}
|
|
|
|
cpuMemoryHotplug(resources = this) {
|
|
this.$dispatch('promptModal', {
|
|
resources,
|
|
component: 'HarvesterCPUMemoryHotPlugDialog'
|
|
});
|
|
}
|
|
|
|
abortMigrationVM() {
|
|
this.doActionGrowl('abortMigration', {});
|
|
}
|
|
|
|
cancelStorageMigration() {
|
|
this.doActionGrowl('cancelStorageMigration', {});
|
|
}
|
|
|
|
createTemplate(resources = this) {
|
|
this.$dispatch('promptModal', {
|
|
resources,
|
|
component: 'HarvesterCloneTemplate'
|
|
});
|
|
}
|
|
|
|
addHotplugVolume(resources = this) {
|
|
this.$dispatch('promptModal', {
|
|
resources,
|
|
component: 'HarvesterAddHotplugVolumeModal'
|
|
});
|
|
}
|
|
|
|
insertCdRomVolume(diskName) {
|
|
const resources = this;
|
|
|
|
this.$dispatch('promptModal', {
|
|
resources,
|
|
name: diskName,
|
|
component: 'HarvesterInsertCdRomVolume',
|
|
});
|
|
}
|
|
|
|
addHotplugNic(resources = this) {
|
|
this.$dispatch('promptModal', {
|
|
resources,
|
|
component: 'HarvesterAddHotplugNic'
|
|
});
|
|
}
|
|
|
|
get networksName() {
|
|
const interfaces = this.spec.template.spec.domain.devices?.interfaces || [];
|
|
|
|
return interfaces.map((I) => I.name);
|
|
}
|
|
|
|
get isOff() {
|
|
return !this.isVMExpectedRunning ? { status: OFF } : null;
|
|
}
|
|
|
|
get isWaitingForVMI() {
|
|
if (this && this.isVMExpectedRunning && !this.isVMCreated) {
|
|
return { status: WAITING, message: VMI_WAITING_MESSAGE };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
get cpuPinningFeatureEnabled() {
|
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('cpuPinning');
|
|
}
|
|
|
|
get isCpuPinning() {
|
|
return this.spec?.template?.spec?.domain?.cpu?.dedicatedCpuPlacement === true;
|
|
}
|
|
|
|
get isVMExpectedRunning() {
|
|
if (!this?.spec) {
|
|
return false;
|
|
}
|
|
const { running = null, runStrategy = null } = this.spec;
|
|
const conditions = this?.status?.conditions || [];
|
|
|
|
if (running) {
|
|
return true;
|
|
}
|
|
|
|
if (runStrategy !== null) {
|
|
let changeRequests;
|
|
|
|
switch (runStrategy) {
|
|
case RunStrategy.Halted:
|
|
return false;
|
|
case RunStrategy.Always:
|
|
return true;
|
|
case RunStrategy.RerunOnFailure:
|
|
if (
|
|
this.status?.printableStatus === 'ErrorUnschedulable' &&
|
|
conditions.find(
|
|
(C) => C.message && C.message.includes(IgnoreMessages)
|
|
)
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return ['Starting', 'Running'].includes(this.status?.printableStatus);
|
|
case RunStrategy.Manual:
|
|
default:
|
|
changeRequests = new Set(
|
|
(this.status?.stateChangeRequests || []).map(
|
|
(chRequest) => chRequest?.action
|
|
)
|
|
);
|
|
|
|
if (changeRequests.has(StateChangeRequest.Stop)) {
|
|
return false;
|
|
}
|
|
if (changeRequests.has(StateChangeRequest.Start)) {
|
|
return true;
|
|
}
|
|
|
|
if (changeRequests.size === 0) {
|
|
return ['Starting', 'Running'].includes(
|
|
this.status?.printableStatus
|
|
);
|
|
}
|
|
|
|
return this.isVMCreated; // if there is no change request we can assume created is representing running (current and expected)
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
get podResource() {
|
|
const vmiResource = this.$getters['byId'](HCI.VMI, this.id);
|
|
|
|
if (!vmiResource?.metadata?.name) {
|
|
return undefined;
|
|
}
|
|
|
|
return getPodByOwnerName(this.$getters, vmiResource.metadata.namespace, vmiResource.metadata.name);
|
|
}
|
|
|
|
get isPaused() {
|
|
const conditions = this.vmi?.status?.conditions || [];
|
|
const isPause = conditions.filter((cond) => cond.type === PAUSED).length > 0;
|
|
|
|
return isPause ? {
|
|
status: PAUSED,
|
|
message: PAUSED_VM_MODAL_MESSAGE
|
|
} : null;
|
|
}
|
|
|
|
get isVMError() {
|
|
const conditions = get(this, 'status.conditions');
|
|
const vmFailureCond = findBy(conditions, 'type', 'Failure');
|
|
|
|
if (vmFailureCond) {
|
|
return {
|
|
status: VM_ERROR,
|
|
detailedMessage: vmFailureCond.message
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
get nsResourceQuota() {
|
|
const allResQuotas = this.$getters['all'](HCI.RESOURCE_QUOTA);
|
|
|
|
return allResQuotas.find( (RQ) => RQ.namespace === this.metadata.namespace);
|
|
}
|
|
|
|
get snapshotSizeQuota() {
|
|
return this.nsResourceQuota?.spec?.snapshotLimit?.vmTotalSnapshotSizeQuota?.[this.metadata.name];
|
|
}
|
|
|
|
get vmi() {
|
|
return this.$getters['byId'](HCI.VMI, this.id);
|
|
}
|
|
|
|
get volumes() {
|
|
const volumeClaimNames = this.spec.template.spec.volumes?.map((v) => v.persistentVolumeClaim?.claimName).filter((v) => !!v) || [];
|
|
|
|
return getPvcsByNames(this.$getters, this.metadata.namespace, volumeClaimNames);
|
|
}
|
|
|
|
get lvmVolumes() {
|
|
return this.volumes.filter((volume) => volume?.isLvm);
|
|
}
|
|
|
|
get longhornV2Volumes() {
|
|
return this.volumes.filter((volume) => volume?.isLonghornV2);
|
|
}
|
|
|
|
get cloneBackendStorageStatus() {
|
|
return this.metadata?.annotations?.[HCI_ANNOTATIONS.CLONE_BACKEND_STORAGE_STATUS]?.toLowerCase() || '';
|
|
}
|
|
|
|
get isCloneBackendStorageCloning() {
|
|
return this.cloneBackendStorageStatus === 'cloning';
|
|
}
|
|
|
|
get isCloneBackendStorageFailed() {
|
|
return this.cloneBackendStorageStatus === 'failed';
|
|
}
|
|
|
|
get encryptedVolumeType() {
|
|
const volumes = this.volumes;
|
|
|
|
if (!volumes || volumes.length === 0) {
|
|
return 'none';
|
|
}
|
|
|
|
if (volumes.every((vol) => vol.isEncrypted)) {
|
|
return 'all';
|
|
} else if (volumes.some((vol) => vol.isEncrypted)) {
|
|
return 'partial';
|
|
} else {
|
|
return 'none';
|
|
}
|
|
}
|
|
|
|
get isError() {
|
|
const conditions = get(this.vmi, 'status.conditions');
|
|
const vmiFailureCond = findBy(conditions, 'type', 'Failure');
|
|
|
|
if (vmiFailureCond) {
|
|
return { status: 'VMI error', detailedMessage: vmiFailureCond.message };
|
|
}
|
|
|
|
return this?.vmi?.status?.phase;
|
|
}
|
|
|
|
get isRunning() {
|
|
const conditions = get(this.vmi, 'status.conditions');
|
|
const isVMIReady = findBy(conditions, 'type', 'Ready')?.status === 'True';
|
|
|
|
if (this.vmi?.status?.phase === VMIPhase.Running && isVMIReady) {
|
|
return { status: VMIPhase.Running };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
get isNotReady() {
|
|
const conditions = get(this.vmi, 'status.conditions');
|
|
const VMIReadyCondition = findBy(conditions, 'type', 'Ready');
|
|
|
|
if (
|
|
VMIReadyCondition?.status === 'False' &&
|
|
this.vmi?.status?.phase === VMIPhase.Running
|
|
) {
|
|
return { status: NOT_READY };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
get isPending() {
|
|
if ((this &&
|
|
!this.isVMExpectedRunning &&
|
|
this.isVMCreated &&
|
|
this.vmi?.status?.phase === VMIPhase.Pending
|
|
) || this.isCloneBackendStorageCloning) {
|
|
return { status: VMIPhase.Pending };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
get isCloneFailed() {
|
|
if (this.isCloneBackendStorageFailed) {
|
|
return { status: VMIPhase.Failed };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
get isStopping() {
|
|
if (this &&
|
|
!this.isVMExpectedRunning &&
|
|
this.isVMCreated &&
|
|
this.vmi?.status?.phase !== undefined &&
|
|
this.vmi?.status?.phase !== VMIPhase.Succeeded &&
|
|
this.vmi?.status?.phase !== VMIPhase.Pending
|
|
) {
|
|
return { status: STOPPING };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
get isStarting() {
|
|
if (this.isVMExpectedRunning && this.isVMCreated) {
|
|
// created but not yet ready
|
|
if (this.podResource) {
|
|
const podStatus = this.podResource.getPodStatus;
|
|
|
|
if (!POD_STATUS_ALL_READY.includes(podStatus?.status)) {
|
|
return {
|
|
...podStatus,
|
|
status: 'Starting',
|
|
message: STARTING_MESSAGE,
|
|
detailedMessage: podStatus?.message,
|
|
pod: this.podResource
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
status: 'Starting',
|
|
message: STARTING_MESSAGE,
|
|
pod: this.podResource
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
get isUnschedulable() {
|
|
// Check the cheap condition first
|
|
const condition = this.status?.conditions?.find((c) => c.reason === UNSCHEDULABLE);
|
|
|
|
if (condition && (this.isStopping || this.isStarting)) {
|
|
return {
|
|
status: UNSCHEDULABLE,
|
|
message: condition.message || 'VM is unschedulable',
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
get isTerminating() {
|
|
return !!this?.metadata?.deletionTimestamp;
|
|
}
|
|
|
|
get otherState() {
|
|
const state = (this.vmi &&
|
|
[VMIPhase.Scheduling, VMIPhase.Scheduled].includes(
|
|
this.vmi?.status?.phase
|
|
) && {
|
|
status: 'Starting',
|
|
message: STARTING_MESSAGE
|
|
}) ||
|
|
(this.vmi &&
|
|
this.vmi.status?.phase === VMIPhase.Pending && {
|
|
status: 'VMI_WAITING',
|
|
message: VMI_WAITING_MESSAGE
|
|
}) ||
|
|
(this.vmi &&
|
|
this.vmi?.status?.phase === VMIPhase.Failed && { status: 'VMI_ERROR' }) ||
|
|
(this.isVMExpectedRunning &&
|
|
!this.isVMCreated && { status: 'Pending' }) || { status: 'UNKNOWN' };
|
|
|
|
return state;
|
|
}
|
|
|
|
get isVMCreated() {
|
|
return !!this?.status?.created;
|
|
}
|
|
|
|
get getDataVolumeTemplates() {
|
|
return get(this, 'spec.volumeClaimTemplates') === null ? [] : this.spec.volumeClaimTemplates;
|
|
}
|
|
|
|
get restoreResource() {
|
|
const id = `${ this.metadata.namespace }/${ get(
|
|
this,
|
|
`metadata.annotations."${ HCI_ANNOTATIONS.RESTORE_NAME }"`
|
|
) }`;
|
|
|
|
const res = this.$getters['byId'](HCI.RESTORE, id);
|
|
|
|
if (res) {
|
|
const backupId = `${ res.spec?.virtualMachineBackupNamespace }/${ res.spec?.virtualMachineBackupName }`;
|
|
const backup = this.$getters['byId'](HCI.BACKUP, backupId);
|
|
|
|
// Snapshots and backups are told apart by spec.type
|
|
res.fromSnapshot = !!backup && backup.spec?.type !== BACKUP_TYPE.BACKUP;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
get restoreProgress() {
|
|
if (this.isVMError || this.isTerminating) {
|
|
return {};
|
|
}
|
|
|
|
const status = this.restoreResource?.status;
|
|
|
|
if (status !== undefined) {
|
|
return {
|
|
type: 'restore',
|
|
percentage: status?.progress || 0,
|
|
details: { volumes: status?.restores || [] }
|
|
};
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
get restoreState() {
|
|
const restore = this.restoreResource;
|
|
|
|
if (!restore) {
|
|
return true;
|
|
}
|
|
|
|
return restore.isComplete;
|
|
}
|
|
|
|
get actualState() {
|
|
if (!this.restoreState) {
|
|
return 'Restoring';
|
|
}
|
|
|
|
if (this.isTerminating) {
|
|
return 'Terminating';
|
|
}
|
|
|
|
if (
|
|
!!this?.vmi?.migrationState &&
|
|
this.vmi.migrationState.status !== 'Failed'
|
|
) {
|
|
return this.vmi.migrationState.status;
|
|
}
|
|
|
|
const state =
|
|
this.isUnschedulable?.status ||
|
|
this.isPaused?.status ||
|
|
this.isVMError?.status ||
|
|
this.isCloneFailed?.status ||
|
|
this.isPending?.status ||
|
|
this.isStopping?.status ||
|
|
this.isOff?.status ||
|
|
this.isError?.status ||
|
|
this.isRunning?.status ||
|
|
this.isNotReady?.status ||
|
|
this.isStarting?.status ||
|
|
this.isWaitingForVMI?.status ||
|
|
this.otherState?.status;
|
|
|
|
return state;
|
|
}
|
|
|
|
get warningMessage() {
|
|
if (this.metadata?.annotations[HCI_ANNOTATIONS.VM_INSUFFICIENT]) {
|
|
return {
|
|
message: this.metadata?.annotations[HCI_ANNOTATIONS.VM_INSUFFICIENT],
|
|
canDismiss: true,
|
|
};
|
|
}
|
|
|
|
const conditions = get(this, 'status.conditions');
|
|
const vmFailureCond = findBy(conditions, 'type', 'Failure');
|
|
|
|
if (vmFailureCond) {
|
|
return {
|
|
status: VM_ERROR,
|
|
message: vmFailureCond.message
|
|
};
|
|
}
|
|
|
|
const vmiConditions = get(this.vmi, 'status.conditions');
|
|
const vmiFailureCond = findBy(vmiConditions, 'type', 'Failure');
|
|
|
|
if (vmiFailureCond) {
|
|
return { status: 'VMI error', detailedMessage: vmiFailureCond.message };
|
|
}
|
|
|
|
if ((this.vmi || this.isVMCreated) && this.podResource) {
|
|
const podStatus = this.podResource.getPodStatus;
|
|
|
|
if (POD_STATUS_ALL_ERROR.includes(podStatus?.status)) {
|
|
return {
|
|
...podStatus,
|
|
status: 'LAUNCHER_POD_ERROR',
|
|
pod: this.podResource
|
|
};
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
get migrationMessage() {
|
|
if (
|
|
!!this?.vmi?.migrationState &&
|
|
this.vmi.migrationState.status === 'Failed'
|
|
) {
|
|
return {
|
|
...this.actualState,
|
|
message: this.t('harvester.modal.migration.failedMessage')
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
get stateDisplay() {
|
|
return this.actualState;
|
|
}
|
|
|
|
get stateColor() {
|
|
const state = this.actualState;
|
|
|
|
return colorForState(state);
|
|
}
|
|
|
|
// Evaluate actualState once, the base implementation does it in stateColor and stateDisplay
|
|
get stateSort() {
|
|
const state = this.actualState;
|
|
|
|
return stateSort(colorForState(state), state);
|
|
}
|
|
|
|
get networkIps() {
|
|
let networkData = '';
|
|
const out = [];
|
|
const arrVolumes = this.spec.template?.spec?.volumes || [];
|
|
|
|
arrVolumes.forEach((V) => {
|
|
if (V.cloudInitNoCloud) {
|
|
networkData = V.cloudInitNoCloud.networkData;
|
|
}
|
|
});
|
|
|
|
try {
|
|
const newInitScript = load(networkData);
|
|
|
|
if (newInitScript?.config && Array.isArray(newInitScript.config)) {
|
|
const config = newInitScript.config;
|
|
|
|
config.forEach((O) => {
|
|
if (O?.subnets && Array.isArray(O.subnets)) {
|
|
const subnets = O.subnets;
|
|
|
|
subnets.forEach((S) => {
|
|
if (S.address) {
|
|
out.push(S.address);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
} catch (err) {}
|
|
|
|
return out;
|
|
}
|
|
|
|
get volumeClaimTemplates() {
|
|
return parseVolumeClaimTemplates(this);
|
|
}
|
|
|
|
get persistentVolumeClaimName() {
|
|
const volumes = this.spec.template.spec.volumes || [];
|
|
|
|
return volumes
|
|
.map((O) => {
|
|
return O?.persistentVolumeClaim?.claimName;
|
|
})
|
|
.filter((name) => !!name);
|
|
}
|
|
|
|
get rootImageId() {
|
|
let imageId = '';
|
|
|
|
const volumes = this.spec.template.spec.volumes || [];
|
|
|
|
const firstVolumeName = volumes[0]?.persistentVolumeClaim?.claimName;
|
|
const isNoExistingVolume = this.volumeClaimTemplates.find((volume) => {
|
|
return firstVolumeName === volume?.metadata?.name;
|
|
});
|
|
|
|
if (!isNoExistingVolume) {
|
|
const existingVolume = this.$getters['byId'](PVC, `${ this.metadata.namespace }/${ firstVolumeName }`);
|
|
|
|
if (existingVolume) {
|
|
return existingVolume?.metadata?.annotations?.[
|
|
'harvesterhci.io/imageId'
|
|
];
|
|
}
|
|
}
|
|
|
|
this.volumeClaimTemplates.find((volume) => {
|
|
imageId = volume?.metadata?.annotations?.['harvesterhci.io/imageId'];
|
|
|
|
return !!imageId;
|
|
});
|
|
|
|
return imageId;
|
|
}
|
|
|
|
get restoreName() {
|
|
return (
|
|
get(this, `metadata.annotations."${ HCI_ANNOTATIONS.RESTORE_NAME }"`) || ''
|
|
);
|
|
}
|
|
|
|
get customValidationRules() {
|
|
const rules = [
|
|
{
|
|
nullable: false,
|
|
path: 'metadata.name',
|
|
required: true,
|
|
minLength: 1,
|
|
maxLength: 63,
|
|
translationKey: 'harvester.fields.name'
|
|
},
|
|
{
|
|
nullable: false,
|
|
path: 'spec.template.spec',
|
|
validators: ['vmNetworks']
|
|
},
|
|
{
|
|
nullable: false,
|
|
path: 'spec',
|
|
validators: [`vmDisks`]
|
|
}
|
|
];
|
|
|
|
return rules;
|
|
}
|
|
|
|
get attachNetwork() {
|
|
const networks = this.spec?.template?.spec?.networks || [];
|
|
const hasMultus = networks.find((N) => N.multus);
|
|
|
|
return !!hasMultus;
|
|
}
|
|
|
|
get memorySort() {
|
|
const memory = getVmCPUMemoryValues(this).memory;
|
|
|
|
const formatSize = parseSi(memory);
|
|
|
|
return parseInt(formatSize, 10);
|
|
}
|
|
|
|
get ingoreVMMessage() {
|
|
const ignoreConditions = [
|
|
{
|
|
name: 'unavailable',
|
|
error: false,
|
|
vmState: this.actualState === PAUSED
|
|
}
|
|
];
|
|
|
|
const state = this.metadata?.state;
|
|
|
|
return (
|
|
ignoreConditions.find(
|
|
(condition) => condition.name === state?.name &&
|
|
condition.error === state?.error &&
|
|
condition.vmState
|
|
) ||
|
|
IgnoreMessages.find((M) => super.stateDescription?.includes(M)) ||
|
|
this.isOff
|
|
);
|
|
}
|
|
|
|
get isRestartRequired() {
|
|
const conditions = get(this, 'status.conditions');
|
|
const restartRequired = findBy(conditions, 'type', 'RestartRequired');
|
|
|
|
return restartRequired && restartRequired.status === 'True';
|
|
}
|
|
|
|
get stateDescription() {
|
|
if (this.isRestartRequired) {
|
|
return this.t('harvester.virtualMachine.hotplug.restartVMMessage');
|
|
}
|
|
|
|
const restoreConditions = this.restoreResource?.status?.conditions || [];
|
|
const restoreError = restoreConditions.find((s) => s.error === true && s.reason === 'Error' && s.type === 'Ready');
|
|
|
|
if (restoreError?.message) {
|
|
return restoreError.message;
|
|
}
|
|
|
|
return this.ingoreVMMessage ? '' : super.stateDescription;
|
|
}
|
|
|
|
get displayCPU() {
|
|
return getVmCPUMemoryValues(this).cpu;
|
|
}
|
|
|
|
get displayMemory() {
|
|
return getVmCPUMemoryValues(this).memory;
|
|
}
|
|
|
|
get isQemuInstalled() {
|
|
const conditions = this.vmi?.status?.conditions || [];
|
|
const qemu = conditions.find((cond) => cond.type === AGENT_CONNECTED);
|
|
|
|
return qemu?.status === 'True';
|
|
}
|
|
|
|
get instanceLabels() {
|
|
const all = this.spec?.template?.metadata?.labels || {};
|
|
|
|
return omitBy(all, (value, key) => {
|
|
return matchesSomeRegex(key, LABELS_TO_IGNORE_REGEX);
|
|
});
|
|
}
|
|
|
|
get hostDevices() {
|
|
return this.spec?.template?.spec?.domain?.devices?.hostDevices || [];
|
|
}
|
|
|
|
get provisionedVGpus() {
|
|
try {
|
|
const deviceAllocationDetails = JSON.parse(this.metadata?.annotations[HCI_ANNOTATIONS.VM_DEVICE_ALLOCATION_DETAILS] || '{}');
|
|
|
|
return deviceAllocationDetails?.gpus || {};
|
|
} catch (error) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
get provisionedHostDevices() {
|
|
// Rancher-provisioned VMs store hostDevices.name as a placeholder ('provisioned'); the
|
|
// real allocated PCI device names live in this annotation, keyed by resourceName.
|
|
try {
|
|
const deviceAllocationDetails = JSON.parse(this.metadata?.annotations[HCI_ANNOTATIONS.VM_DEVICE_ALLOCATION_DETAILS] || '{}');
|
|
|
|
return deviceAllocationDetails?.hostdevices || {};
|
|
} catch (error) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
get schedulingVMBackupFeatureEnabled() {
|
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('schedulingVMBackup');
|
|
}
|
|
|
|
get nameDisplay() {
|
|
return this.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME] || this.metadata?.name || this.id;
|
|
}
|
|
|
|
get volumeEncryptionFeatureEnabled() {
|
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
|
|
}
|
|
|
|
get tpmPersistentStateFeatureEnabled() {
|
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('tpmPersistentState');
|
|
}
|
|
|
|
get efiPersistentStateFeatureEnabled() {
|
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('efiPersistentState');
|
|
}
|
|
|
|
get thirdPartyStorageFeatureEnabled() {
|
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
|
}
|
|
|
|
get vmMachineTypeAutoFeatureEnabled() {
|
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('vmMachineTypeAuto');
|
|
}
|
|
|
|
get hotplugNicFeatureEnabled() {
|
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('hotplugNic');
|
|
}
|
|
|
|
get hotplugCdRomEnabled() {
|
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('hotplugCdRom');
|
|
}
|
|
|
|
get isBackupTargetUnavailable() {
|
|
const backupTargetSetting = this.$rootGetters['harvester/byId'](HCI.SETTING, 'backup-target');
|
|
|
|
// sharedComputed needs an object as key
|
|
if (!backupTargetSetting) {
|
|
return isBackupTargetSettingUnavailable(backupTargetSetting);
|
|
}
|
|
|
|
return sharedComputed(backupTargetSetting, 'unavailable', isBackupTargetSettingUnavailable);
|
|
}
|
|
|
|
setInstanceLabels(val) {
|
|
if ( !this.spec?.template?.metadata?.labels ) {
|
|
set(this, 'spec.template.metadata.labels', {});
|
|
}
|
|
|
|
const all = this.spec.template.metadata.labels || {};
|
|
const wasIgnored = pickBy(all, (value, key) => {
|
|
return matchesSomeRegex(key, LABELS_TO_IGNORE_REGEX);
|
|
});
|
|
|
|
this.spec.template.metadata['labels'] = { ...wasIgnored, ...val };
|
|
}
|
|
}
|