Add pkg/harvester components + shell portings - 1

Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
Francesco Torchia
2024-10-23 17:00:46 +02:00
parent f8408469f7
commit 4f2688f6ab
265 changed files with 42316 additions and 82 deletions
@@ -0,0 +1,24 @@
import { MANAGEMENT } from '@shell/config/types';
import NormanModel from '@shell/plugins/steve/norman-class';
export default class CRTB extends NormanModel {
get clusterroletemplatebinding() {
return this.$rootGetters[`management/byId`](MANAGEMENT.CLUSTER_ROLE_TEMPLATE_BINDING, this.id?.replace(':', '/'));
}
get principalId() {
return this.userPrincipalId || this.groupPrincipalId;
}
get roleDisplay() {
return this.roleTemplate.nameDisplay;
}
get roleDescription() {
return this.roleTemplate.description;
}
get roleTemplate() {
return this.$rootGetters['management/byId'](MANAGEMENT.ROLE_TEMPLATE, this.roleTemplateId);
}
}
@@ -0,0 +1,170 @@
import SteveModel from '@shell/plugins/steve/steve-class';
import { escapeHtml } from '@shell/utils/string';
import { HCI } from '../types';
const STATUS_DISPLAY = {
enabled: {
displayKey: 'generic.enabled',
color: 'bg-success'
},
pending: {
displayKey: 'generic.inProgress',
color: 'bg-info'
},
disabled: {
displayKey: 'generic.disabled',
color: 'bg-warning'
},
error: {
displayKey: 'generic.disabled',
color: 'bg-warning'
}
};
/**
* Class representing PCI Device resource.
* @extends SteveModal
*/
export default class PCIDevice extends SteveModel {
get _availableActions() {
const out = super._availableActions;
out.push(
{
action: 'enablePassthroughBulk',
enabled: !this.isEnabling,
icon: 'icon icon-fw icon-dot',
label: 'Enable Passthrough',
bulkable: true,
bulkAction: 'enablePassthroughBulk'
},
{
action: 'disablePassthrough',
enabled: this.isEnabling && this.claimedByMe,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable Passthrough',
bulkable: true
},
);
return out;
}
get canYaml() {
return false;
}
get canDelete() {
return false;
}
goToDetail() {
return false;
}
goToEdit() {
return false;
}
get passthroughClaim() {
const passthroughClaims = this.$getters['all'](HCI.PCI_CLAIM) || [];
return !!this.status && passthroughClaims.find(req => req?.spec?.nodeName === this.status?.nodeName && req?.spec?.address === this.status?.address);
}
// this is an id for each 'type' of device - there may be multiple instances of device CRs
get uniqueId() {
return `${ this.status?.vendorId }:${ this.status?.deviceId }`;
}
get claimedBy() {
return this.passthroughClaim?.spec?.userName;
}
get claimedByMe() {
if (!this.passthroughClaim) {
return false;
}
const isSingleProduct = this.$rootGetters['isSingleProduct'];
let userName = 'admin';
// if this is imported Harvester, there may be users other than admin
if (!isSingleProduct) {
const user = this.$rootGetters['auth/v3User'];
userName = user?.username || user?.id;
}
return this.claimedBy === userName;
}
// isEnabled controls visibility in vm create page & ability to delete claim
// isEnabling controls ability to add claim
// there will be a brief period where isEnabling === true && isEnabled === false
get isEnabled() {
return !!this.passthroughClaim?.status?.passthroughEnabled;
}
get isEnabling() {
return !!this.passthroughClaim;
}
// map status.passthroughEnabled to disabled/enabled & overwrite default dash colors
get claimStatusDisplay() {
if (!this.passthroughClaim) {
return STATUS_DISPLAY.disabled;
}
if (this.isEnabled) {
return STATUS_DISPLAY.enabled;
}
return STATUS_DISPLAY.pending;
}
get stateDisplay() {
const t = this.$rootGetters['i18n/t'];
return t(this.claimStatusDisplay.displayKey);
}
get stateBackground() {
return this.claimStatusDisplay.color;
}
// 'enable' passthrough creates the passthrough claim CRD -
enablePassthroughBulk(resources = this) {
this.$dispatch('promptModal', {
resources,
component: 'EnablePassthrough'
});
}
// 'disable' passthrough deletes claim
// backend should return error if device is in use
async disablePassthrough() {
try {
if (!this.claimedByMe) {
throw new Error(this.$rootGetters['i18n/t']('harvester.pci.cantUnclaim', { name: escapeHtml(this.metadata.name) }));
} else {
await this.passthroughClaim.remove();
}
} catch (err) {
this.$dispatch('growl/fromError', {
title: this.$rootGetters['i18n/t']('harvester.pci.unclaimError', { name: escapeHtml(this.metadata.name) }),
err,
}, { root: true });
}
}
// group device list by node
get groupByNode() {
const name = this.status?.nodeName || this.$rootGetters['i18n/t']('generic.none');
return this.$rootGetters['i18n/t']('resourceTable.groupLabel.node', { name: escapeHtml(name) });
}
// group device list by unique device (same vendorid and deviceid)
get groupByDevice() {
return this.status?.description;
}
}
@@ -0,0 +1,130 @@
import SteveModel from '@shell/plugins/steve/steve-class';
import { escapeHtml } from '@shell/utils/string';
import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
import { NODE } from '@shell/config/types';
import { HCI } from '../types';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
/**
* Class representing SR-IOV Device resource.
* @extends SteveModal
*/
export default class SRIOVDevice extends SteveModel {
get _availableActions() {
const out = super._availableActions;
out.push(
{
action: 'enableDevice',
enabled: !this.isEnabled,
icon: 'icon icon-fw icon-dot',
label: 'Enable',
},
{
action: 'disableDevice',
enabled: this.isEnabled,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable',
},
);
return out;
}
get canYaml() {
return false;
}
get canDelete() {
return false;
}
goToDetail() {
return false;
}
goToEdit() {
return false;
}
get actualState() {
return this.isEnabled ? 'Enabled' : 'Disabled';
}
get stateDisplay() {
return this.actualState;
}
get stateColor() {
const state = this.actualState;
return colorForState(state);
}
get isEnabled() {
return this.spec.enabled && this.status?.vfAddresses?.length > 0 && this.status?.vGPUDevices?.length > 0;
}
async enableDevice() {
try {
this.spec.enabled = true;
await this.save();
} catch (err) {
this.$dispatch('growl/fromError', {
title: this.t('generic.notification.title.error', { name: escapeHtml(this.metadata.name) }),
err,
}, { root: true });
}
}
async disableDevice() {
const inStore = this.$rootGetters['currentProduct'].inStore;
const schema = this.$rootGetters[`${ inStore }/schemaFor`](HCI.VGPU_DEVICE);
if (!!schema) {
const vGpuDevices = this.$rootGetters[`${ inStore }/all`](HCI.VGPU_DEVICE) || [];
const vGpuDevicesEnabled = vGpuDevices
.filter(f => f.labels[HCI_ANNOTATIONS.PARENT_SRIOV_GPU] === this.id && f.spec?.enabled)
.map(m => m.id);
if (vGpuDevicesEnabled.length > 0) {
this.$dispatch('growl/error', {
title: this.t('generic.notification.title.error', { name: escapeHtml(this.metadata.name) }),
message: `
Cannot disable ${ this.metadata.name }, following vGPU devices are enabled:
[${ vGpuDevicesEnabled.join(', ') }]
`,
}, { root: true });
return;
}
}
try {
this.spec.enabled = false;
await this.save();
} catch (err) {
this.$dispatch('growl/fromError', {
title: this.t('generic.notification.title.error', { name: escapeHtml(this.metadata.name) }),
err,
}, { root: true });
}
}
get realNodeName() {
const inStore = this.$rootGetters['currentProduct'].inStore;
const nodeName = this.spec?.nodeName;
const nodes = this.$rootGetters[`${ inStore }/all`](NODE);
const node = nodes.find(N => N.id === nodeName);
return node?.nameDisplay || '';
}
get numVFs() {
return this.status?.vfAddresses?.length || 0;
}
get childDevice() {
return HCI.VGPU_DEVICE;
}
}
@@ -0,0 +1,105 @@
import SteveModel from '@shell/plugins/steve/steve-class';
import { escapeHtml } from '@shell/utils/string';
import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
import { NODE } from '@shell/config/types';
import { HCI } from '../types';
/**
* Class representing SR-IOV Device resource.
* @extends SteveModal
*/
export default class SRIOVGpuDevice extends SteveModel {
get _availableActions() {
const out = super._availableActions;
out.push(
{
action: 'enableDevice',
enabled: !this.isEnabled,
icon: 'icon icon-fw icon-dot',
label: 'Enable',
},
{
action: 'disableDevice',
enabled: this.isEnabled,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable',
},
);
return out;
}
get canYaml() {
return false;
}
get canDelete() {
return false;
}
goToDetail() {
return false;
}
goToEdit() {
return false;
}
get actualState() {
return this.isEnabled ? 'Enabled' : 'Disabled';
}
get stateDisplay() {
return this.actualState;
}
get stateColor() {
const state = this.actualState;
return colorForState(state);
}
get isEnabled() {
return this.status?.status === 'sriovNetworkDeviceEnabled' && this.spec?.numVFs > 0;
}
enableDevice(resources = this) {
this.$dispatch('promptModal', {
resources,
component: 'EnableSriovDevice'
});
}
async disableDevice() {
const numVFsHistory = this.spec.numVFs;
try {
this.spec.numVFs = 0;
await this.save();
} catch (err) {
this.spec.numVFs = numVFsHistory;
this.$dispatch('growl/fromError', {
title: this.t('generic.notification.title.error', { name: escapeHtml(this.metadata.name) }),
err,
}, { root: true });
}
}
get realNodeName() {
const inStore = this.$rootGetters['currentProduct'].inStore;
const nodeName = this.spec?.nodeName;
const nodes = this.$rootGetters[`${ inStore }/all`](NODE);
const node = nodes.find(N => N.id === nodeName);
return node?.nameDisplay || '';
}
get numVFs() {
return this.spec?.numVFs;
}
get childDevice() {
return HCI.PCI_DEVICE;
}
}
@@ -0,0 +1,129 @@
import SteveModel from '@shell/plugins/steve/steve-class';
import { escapeHtml } from '@shell/utils/string';
const STATUS_DISPLAY = {
enabled: {
displayKey: 'generic.enabled',
color: 'bg-success'
},
pending: {
displayKey: 'generic.inProgress',
color: 'bg-info'
},
disabled: {
displayKey: 'generic.disabled',
color: 'bg-warning'
},
error: {
displayKey: 'generic.disabled',
color: 'bg-warning'
}
};
/**
* Class representing PCI Device resource.
* @extends SteveModal
*/
export default class VGpuDevice extends SteveModel {
get _availableActions() {
const out = super._availableActions;
out.push(
{
action: 'enableVGpu',
enabled: !this.isEnabled,
icon: 'icon icon-fw icon-dot',
label: 'Enable',
},
{
action: 'disableVGpu',
enabled: this.isEnabled,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable',
bulkable: true,
},
);
return out;
}
get canYaml() {
return false;
}
get canDelete() {
return false;
}
goToDetail() {
return false;
}
goToEdit() {
return false;
}
get isEnabled() {
return this?.spec?.enabled === true;
}
get isEnabling() {
return this.status?.vGPUStatus && this.status?.vGPUStatus !== 'vGPUConfigured';
}
get statusDisplay() {
if (this.isEnabling) {
return STATUS_DISPLAY.pending;
}
if (this.isEnabled) {
return STATUS_DISPLAY.enabled;
}
return STATUS_DISPLAY.disabled;
}
get stateDisplay() {
const t = this.$rootGetters['i18n/t'];
return t(this.statusDisplay.displayKey);
}
get stateBackground() {
return this.statusDisplay.color;
}
enableVGpu(resources = this) {
this.$dispatch('promptModal', {
resources,
component: 'EnableVGpuDevice'
});
}
async disableVGpu() {
const { vGPUTypeName, enabled } = this.spec;
try {
this.spec.vGPUTypeName = undefined;
this.spec.enabled = false;
await this.save();
} catch (err) {
this.spec.vGPUTypeName = vGPUTypeName;
this.spec.enabled = enabled;
this.$dispatch('growl/fromError', {
title: this.t('generic.notification.title.error', { name: escapeHtml(this.metadata.name) }),
err,
}, { root: true });
}
}
get groupByNode() {
const name = this.spec?.nodeName || this.$rootGetters['i18n/t']('generic.none');
return this.$rootGetters['i18n/t']('resourceTable.groupLabel.node', { name: escapeHtml(name) });
}
get vGpuAvailableTypes() {
return this.status?.availableTypes ? Object.keys(this.status.availableTypes) : [];
}
}
+69
View File
@@ -0,0 +1,69 @@
import SteveModel from '@shell/plugins/steve/steve-class';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
import { VIEW_IN_API, DEV } from '@shell/store/prefs';
export default class HarvesterResource extends SteveModel {
get listLocation() {
const name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
return this.$rootGetters['type-map/optionsFor'](this.type).customRoute || {
name,
params: {
product: this.$rootGetters['productId'],
cluster: this.$rootGetters['clusterId'],
resource: this.type,
},
};
}
get parentLocationOverride() {
return this.listLocation;
}
get doneRoute() {
return this.listLocation.name;
}
get doneOverride() {
return this.listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get _detailLocation() {
const schema = this.$getters['schemaFor'](this.type);
const id = this.id?.replace(/.*\//, '');
const name = this.harvesterResourcesInExplorer ? `c-cluster-product-resource${ schema?.attributes?.namespaced ? '-namespace' : '' }-id` : `${ HARVESTER_PRODUCT }-c-cluster-resource${ schema?.attributes?.namespaced ? '-namespace' : '' }-id`;
const out = {
name,
params: {
product: this.$rootGetters['productId'],
cluster: this.$rootGetters['clusterId'],
resource: this.type,
namespace: this.metadata?.namespace,
id,
},
};
if (this.harvesterResourcesInExplorer) {
out.query = { as: 'yaml' };
}
return out;
}
get forceYaml() {
return this.harvesterResourcesInExplorer;
}
get canViewInApi() {
try {
return this.hasLink('self') && this.$rootGetters['prefs/get'](VIEW_IN_API);
} catch {
return this.hasLink('self') && this.$rootGetters['prefs/get'](DEV);
}
}
}
@@ -0,0 +1,33 @@
import { clone } from '@shell/utils/object';
import { HCI } from '../../types';
import HarvesterResource from '../harvester';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
export default class HciConfigMap extends HarvesterResource {
get detailLocation() {
const detailLocation = clone(this._detailLocation);
detailLocation.params.resource = HCI.CLOUD_TEMPLATE;
return detailLocation;
}
get doneOverride() {
const detailLocation = clone(this._detailLocation);
delete detailLocation.params.namespace;
delete detailLocation.params.id;
detailLocation.params.resource = HCI.CLOUD_TEMPLATE;
detailLocation.name = `${ HARVESTER_PRODUCT }-c-cluster-resource`;
return detailLocation;
}
get parentNameOverride() {
return this.$rootGetters['i18n/t'](`typeLabel."${ HCI.CLOUD_TEMPLATE }"`, { count: 1 })?.trim();
}
get parentLocationOverride() {
return this.doneOverride;
}
}
@@ -0,0 +1,91 @@
import { clone } from '@shell/utils/object';
import { HCI } from '../../types';
import NetworkAttachmentDef from '@pkg/harvester/models/k8s.cni.cncf.io.networkattachmentdefinition';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
const NOT_READY = 'Not Ready';
export default class HarvesterNetworkAttachmentDef extends NetworkAttachmentDef {
get listLocation() {
return this.$rootGetters['type-map/optionsFor'](this.type).customRoute || {
name: `${ HARVESTER_PRODUCT }-c-cluster-resource`,
params: {
product: HARVESTER_PRODUCT,
cluster: this.$rootGetters['clusterId'],
resource: this.type,
},
};
}
get doneRoute() {
return this.listLocation.name;
}
get detailLocation() {
const detailLocation = clone(this._detailLocation);
detailLocation.params.resource = HCI.NETWORK_ATTACHMENT;
detailLocation.name = `${ HARVESTER_PRODUCT }-c-cluster-resource-namespace-id`;
return detailLocation;
}
get doneOverride() {
const detailLocation = clone(this._detailLocation);
delete detailLocation.params.namespace;
delete detailLocation.params.id;
detailLocation.params.resource = HCI.NETWORK_ATTACHMENT;
detailLocation.name = `${ HARVESTER_PRODUCT }-c-cluster-resource`;
return detailLocation;
}
get parentNameOverride() {
return this.$rootGetters['i18n/t'](`typeLabel."${ HCI.NETWORK_ATTACHMENT }"`, { count: 1 })?.trim();
}
get parentLocationOverride() {
return this.doneOverride;
}
get inStore() {
return this.$rootGetters['currentProduct'].inStore;
}
get clusterNetworkResource() {
const clusterNetworks = this.$rootGetters[`${ this.inStore }/all`](HCI.CLUSTER_NETWORK);
return clusterNetworks.find((c) => c.id === this.clusterNetwork);
}
get clusterNetworkErrorMessage() {
if (!this.clusterNetworkResource) {
return this.t('harvester.clusterNetwork.notExist', { clusterNetwork: this.clusterNetwork });
} else if (!this.clusterNetworkResource.isReady) {
return this.t('harvester.clusterNetwork.notReady', { clusterNetwork: this.clusterNetwork });
} else {
return '';
}
}
get stateDisplay() {
if (this.clusterNetworkErrorMessage) {
return NOT_READY;
}
return super.stateDisplay;
}
get stateBackground() {
if (this.stateDisplay === NOT_READY) {
return 'bg-warning';
}
return super.stateBackground;
}
get isNotReady() {
return this.clusterNetworkErrorMessage;
}
}
@@ -0,0 +1,55 @@
import { clone } from '@shell/utils/object';
import { LOGGING } from '@shell/config/types';
import { HCI } from '../../types';
import HarvesterFlow from './logging.banzaicloud.io.flow';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
export default class HciClusterflow extends HarvesterFlow {
get allOutputs() {
return this.$rootGetters['harvester/all'](LOGGING.CLUSTER_OUTPUT) || [];
}
get _detailLocation() {
const schema = this.$getters['schemaFor'](this.type);
const id = this.id?.replace(/.*\//, '');
return {
name: `${ HARVESTER_PRODUCT }-c-cluster-resource${ schema?.attributes?.namespaced ? '-namespace' : '' }-id`,
params: {
product: HARVESTER_PRODUCT,
cluster: this.$rootGetters['clusterId'],
resource: this.type,
id,
namespace: this.metadata.namespace,
},
};
}
get detailLocation() {
const detailLocation = clone(this._detailLocation);
detailLocation.params.resource = HCI.CLUSTER_FLOW;
return detailLocation;
}
get doneOverride() {
const detailLocation = clone(this._detailLocation);
delete detailLocation.params.namespace;
delete detailLocation.params.id;
detailLocation.params.resource = HCI.CLUSTER_FLOW;
detailLocation.name = `${ HARVESTER_PRODUCT }-c-cluster-resource`;
return detailLocation;
}
get parentNameOverride() {
return this.$rootGetters['i18n/t'](`typeLabel."${ HCI.CLUSTER_FLOW }"`, { count: 1 })?.trim();
}
get parentLocationOverride() {
return this.doneOverride;
}
}
@@ -0,0 +1,50 @@
import { clone } from '@shell/utils/object';
import { HCI } from '../../types';
import LogOutput from './logging.banzaicloud.io.output';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
export default class HciClusteroutput extends LogOutput {
get _detailLocation() {
const schema = this.$getters['schemaFor'](this.type);
const id = this.id?.replace(/.*\//, '');
return {
name: `${ HARVESTER_PRODUCT }-c-cluster-resource${ schema?.attributes?.namespaced ? '-namespace' : '' }-id`,
params: {
product: HARVESTER_PRODUCT,
cluster: this.$rootGetters['clusterId'],
resource: this.type,
id,
namespace: this.metadata.namespace,
},
};
}
get detailLocation() {
const detailLocation = clone(this._detailLocation);
detailLocation.params.resource = HCI.CLUSTER_OUTPUT;
return detailLocation;
}
get doneOverride() {
const detailLocation = clone(this._detailLocation);
delete detailLocation.params.namespace;
delete detailLocation.params.id;
detailLocation.params.resource = HCI.CLUSTER_OUTPUT;
detailLocation.name = `${ HARVESTER_PRODUCT }-c-cluster-resource`;
return detailLocation;
}
get parentNameOverride() {
return this.$rootGetters['i18n/t'](`typeLabel."${ HCI.CLUSTER_OUTPUT }"`, { count: 1 })?.trim();
}
get parentLocationOverride() {
return this.doneOverride;
}
}
@@ -0,0 +1,72 @@
import { LOGGING } from '@shell/config/types';
import { HCI } from '../../types';
import { clone } from '@shell/utils/object';
import Flow from '@shell/models/logging.banzaicloud.io.flow';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
import { FLOW_TYPE } from '../../config/harvester-map';
export default class HarvesterLogFlow extends Flow {
get allOutputs() {
return this.$rootGetters['harvester/all'](LOGGING.OUTPUT) || [];
}
get allClusterOutputs() {
return this.$rootGetters['harvester/all'](LOGGING.CLUSTER_OUTPUT) || [];
}
get _detailLocation() {
const schema = this.$getters['schemaFor'](this.type);
const id = this.id?.replace(/.*\//, '');
return {
name: `${ HARVESTER_PRODUCT }-c-cluster-resource${ schema?.attributes?.namespaced ? '-namespace' : '' }-id`,
params: {
product: HARVESTER_PRODUCT,
cluster: this.$rootGetters['clusterId'],
resource: this.type,
id,
namespace: this.metadata.namespace,
},
};
}
get detailLocation() {
const detailLocation = clone(this._detailLocation);
detailLocation.params.resource = HCI.FLOW;
return detailLocation;
}
get doneOverride() {
const detailLocation = clone(this._detailLocation);
delete detailLocation.params.namespace;
delete detailLocation.params.id;
detailLocation.params.resource = HCI.FLOW;
detailLocation.name = `${ HARVESTER_PRODUCT }-c-cluster-resource`;
return detailLocation;
}
get parentNameOverride() {
return this.$rootGetters['i18n/t'](`typeLabel."${ HCI.FLOW }"`, { count: 1 })?.trim();
}
get parentLocationOverride() {
return this.doneOverride;
}
get loggingType() {
if (this.spec.loggingRef === 'harvester-kube-audit-log-ref') {
return FLOW_TYPE.AUDIT;
}
if ((this.spec.match || []).find(M => !!M?.select?.labels?.['app.kubernetes.io/name'])) {
return FLOW_TYPE.EVENT;
}
return FLOW_TYPE.LOGGING;
}
}
@@ -0,0 +1,63 @@
import { clone } from '@shell/utils/object';
import { HCI } from '../../types';
import LogOutput from '@shell/models/logging.banzaicloud.io.output';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
import { FLOW_TYPE } from '../../config/harvester-map';
export default class HarvesterLogOutput extends LogOutput {
get _detailLocation() {
const schema = this.$getters['schemaFor'](this.type);
const id = this.id?.replace(/.*\//, '');
return {
name: `${ HARVESTER_PRODUCT }-c-cluster-resource${ schema?.attributes?.namespaced ? '-namespace' : '' }-id`,
params: {
product: HARVESTER_PRODUCT,
cluster: this.$rootGetters['clusterId'],
resource: this.type,
id,
namespace: this.metadata.namespace,
},
};
}
get detailLocation() {
const detailLocation = clone(this._detailLocation);
detailLocation.params.resource = HCI.OUTPUT;
return detailLocation;
}
get doneOverride() {
const detailLocation = clone(this._detailLocation);
delete detailLocation.params.namespace;
delete detailLocation.params.id;
detailLocation.params.resource = HCI.OUTPUT;
detailLocation.name = `${ HARVESTER_PRODUCT }-c-cluster-resource`;
return detailLocation;
}
get parentNameOverride() {
return this.$rootGetters['i18n/t'](`typeLabel."${ HCI.OUTPUT }"`, { count: 1 })?.trim();
}
get parentLocationOverride() {
return this.doneOverride;
}
get url() {
return this.detailLocation;
}
get loggingType() {
if (this.spec.loggingRef === 'harvester-kube-audit-log-ref') {
return FLOW_TYPE.AUDIT;
}
return FLOW_TYPE.LOGGING;
}
}
@@ -0,0 +1,45 @@
import { HCI } from '../../types';
import { clone } from '@shell/utils/object';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
import ManagementSetting from '@shell/models/management.cattle.io.setting';
export default class HCIManagementSetting extends ManagementSetting {
get detailLocation() {
const detailLocation = clone(this._detailLocation);
detailLocation.params.resource = HCI.SETTING;
return detailLocation;
}
get doneOverride() {
const detailLocation = clone(this._detailLocation);
delete detailLocation.params.namespace;
delete detailLocation.params.id;
detailLocation.params.resource = HCI.SETTING;
detailLocation.name = `${ HARVESTER_PRODUCT }-c-cluster-resource`;
return detailLocation;
}
get doneRoute() {
return null;
}
get parentNameOverride() {
return this.$rootGetters['i18n/t'](`typeLabel."${ HCI.SETTING }"`, { count: 1 })?.trim();
}
get parentLocationOverride() {
return this.doneOverride;
}
get doneParams() {
return {
product: this.$rootGetters['productId'],
cluster: this.$rootGetters['clusterId'],
resource: HCI.SETTING,
};
}
}
@@ -0,0 +1,142 @@
import { clone, set } from '@shell/utils/object';
import { _YAML, _CREATE, _VIEW, _CONFIG } from '@shell/config/query-params';
import { HCI } from '../../types';
import SteveModel from '@shell/plugins/steve/steve-class';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
export default class HciAlertmanagerConfig extends SteveModel {
get _detailLocation() {
return {
name: this.alertmanagerConfigDoneRouteName,
params: {
cluster: this.$rootGetters['clusterId'],
resource: HCI.ALERTMANAGERCONFIG,
namespace: this.metadata?.namespace,
id: this.name,
},
hash: '#receivers',
query: { as: 'config' }
};
}
get doneOverride() {
const detailLocation = clone(this._detailLocation);
delete detailLocation.params.namespace;
delete detailLocation.params.id;
detailLocation.params.resource = HCI.ALERTMANAGERCONFIG;
detailLocation.name = `${ HARVESTER_PRODUCT }-c-cluster-resource`;
return detailLocation;
}
get parentNameOverride() {
return this.$rootGetters['i18n/t'](`typeLabel."${ HCI.ALERTMANAGERCONFIG }"`, { count: 1 })?.trim();
}
get parentLocationOverride() {
return this.doneOverride;
}
applyDefaults() {
if (this.spec) {
return this.spec;
}
const existingReceivers = this.spec?.route?.receivers || [];
const defaultSpec = {
receivers: [...existingReceivers],
route: {
receivers: this.spec?.route?.receivers || [],
groupBy: this.spec?.route?.groupBy || [],
groupWait: this.spec?.route?.groupWait || '30s',
groupInterval: this.spec?.route?.groupInterval || '5m',
repeatInterval: this.spec?.route?.repeatInterval || '4h',
match: this.spec?.route?.match || {},
matchRe: this.spec?.route?.matchRe || {}
}
};
set(this, 'spec', defaultSpec);
}
get _availableActions() {
const out = super._availableActions;
return out;
}
getReceiverActions(alertmanagerConfigActions) {
return alertmanagerConfigActions.filter((actionData) => {
if (actionData.divider) {
return true;
}
switch (actionData.action) {
case 'goToEdit':
return true;
case 'goToEditYaml':
return true;
case 'promptRemove':
return true;
default:
return false;
}
});
}
get alertmanagerConfigDoneRouteName() {
return `${ HARVESTER_PRODUCT }-c-cluster-resource-namespace-id`;
}
getCreateReceiverRoute() {
return {
name: `${ HARVESTER_PRODUCT }-c-cluster-alertmanagerconfig-alertmanagerconfigid-receiver`,
params: { cluster: this.$rootGetters['clusterId'], alertmanagerconfigid: this.id },
query: { mode: _CREATE, currentView: _CONFIG }
};
}
getReceiverDetailLink(receiverName) {
return {
name: `${ HARVESTER_PRODUCT }-c-cluster-alertmanagerconfig-alertmanagerconfigid-receiver`,
params: {
cluster: this.$rootGetters['clusterId'],
alertmanagerconfigid: this.id,
receiverName
},
query: {
mode: _VIEW, receiverName, currentView: _CONFIG
}
};
}
getEditReceiverYamlRoute(receiverName, queryMode) {
return {
name: `${ HARVESTER_PRODUCT }-c-cluster-alertmanagerconfig-alertmanagerconfigid-receiver`,
params: {
cluster: this.$rootGetters['clusterId'],
alertmanagerconfigid: this.id
},
query: {
mode: queryMode || _VIEW,
receiverName,
currentView: _YAML
}
};
}
getEditReceiverConfigRoute(receiverName, queryMode) {
return {
name: `${ HARVESTER_PRODUCT }-c-cluster-alertmanagerconfig-alertmanagerconfigid-receiver`,
params: {
cluster: this.$rootGetters['clusterId'],
alertmanagerconfigid: this.id
},
query: {
mode: queryMode || _VIEW,
receiverName,
currentView: _CONFIG
}
};
}
}
+110
View File
@@ -0,0 +1,110 @@
import { insertAt } from '@shell/utils/array';
import namespace from '@shell/models/namespace';
import { SYSTEM_NAMESPACE, FLEET } from '@shell/config/labels-annotations';
import SYSTEM_NAMESPACES from '@shell/config/system-namespaces';
import { get } from '@shell/utils/object';
import { NAMESPACE } from '@shell/config/types';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester';
const OBSCURE_NAMESPACE_PREFIX = [
'c-', // cluster namespace
// Project namespace. When a user creates a project, Rancher creates
// namespaces in the local cluster with the 'p-' prefix which are
// used to manage RBAC for the project. If these namespaces are deleted,
// role bindings can be lost and Rancher may need to be restored from
// backup. Therefore we hide these namespaces unless the developer setting
// is turned on from the user preferences.
'p-',
'user-', // user namespace
'local', // local namespace
];
export default class HciNamespace extends namespace {
get _availableActions() {
const out = super._availableActions;
const remove = out.findIndex(a => a.action === 'promptRemove');
const promptRemove = {
action: 'promptRemove',
altAction: 'remove',
label: this.t('action.remove'),
icon: 'icon icon-trash',
bulkable: true,
enabled: this.canDelete,
bulkAction: 'promptRemove',
weight: -10,
};
if (remove > -1) {
out.splice(remove, 1);
}
insertAt(out, out.length - 1, promptRemove);
return out;
}
promptRemove(resources = this) {
this.$dispatch('promptModal', {
resources,
warningMessageKey: 'promptRemove.confirmRelatedResource',
component: 'ConfirmRelatedToRemoveDialog'
});
}
get isSystem() {
const systemNamespaces = ['fleet-default'];
if (systemNamespaces.includes(this.metadata.name)) {
return true;
}
if ( this.metadata?.annotations?.[SYSTEM_NAMESPACE] === 'true' ) {
return true;
}
if ( SYSTEM_NAMESPACES.includes(this.metadata.name) ) {
return true;
}
if ( this.metadata.name.endsWith('-system') ) {
return true;
}
if ( this.project ) {
return this.project.isSystem;
}
if (get(this, `metadata.labels."${ FLEET.MANAGED }"`) === 'true') {
return true;
}
const isSettingSystemNamespace = this.$rootGetters['systemNamespaces'].includes(this.metadata.name);
const isObscurePrefix = !!OBSCURE_NAMESPACE_PREFIX.some(prefix => this.metadata.name.startsWith(prefix));
return isSettingSystemNamespace || isObscurePrefix;
}
get detailLocation() {
const id = this.id?.replace(/.*\//, '');
const detailLocation = {
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-id`,
params: {
product: this.$rootGetters['productId'],
cluster: this.$rootGetters['clusterId'],
resource: NAMESPACE,
namespace: this.metadata?.namespace,
id,
}
};
return detailLocation;
}
get hideDetailLocation() {
return !!this.$rootGetters['currentProduct'].hideNamespaceLocation;
}
}
+607
View File
@@ -0,0 +1,607 @@
import pickBy from 'lodash/pickBy';
import { CAPI, LONGHORN, POD, NODE } from '@shell/config/types';
import { HCI } from '../../types';
import { CAPI as CAPI_ANNOTATIONS } from '@shell/config/labels-annotations.js';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { clone } from '@shell/utils/object';
import findLast from 'lodash/findLast';
import {
colorForState,
stateDisplay,
STATES_ENUM,
} from '@shell/plugins/dashboard-store/resource-class';
import { parseSi } from '@shell/utils/units';
import HarvesterResource from '../harvester';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
import { findBy, isArray } from '@shell/utils/array';
import { ucFirst } from '@shell/utils/string';
const ALLOW_SYSTEM_LABEL_KEYS = [
'topology.kubernetes.io/zone',
'topology.kubernetes.io/region',
];
const HEALTHY = 'healthy';
const WARNING = 'warning';
export default class HciNode extends HarvesterResource {
get _availableActions() {
const cordon = {
action: 'cordon',
enabled: this.hasAction('cordon') && !this.isCordoned,
icon: 'icon icon-fw icon-pause',
label: this.t('harvester.action.cordon'),
total: 1
};
const uncordon = {
action: 'uncordon',
enabled: this.hasAction('uncordon'),
icon: 'icon icon-fw icon-play',
label: this.t('harvester.action.uncordon'),
total: 1
};
const enableMaintenance = {
action: 'enableMaintenanceMode',
enabled: this.hasAction('enableMaintenanceMode'),
icon: 'icon icon-fw icon-unlock',
label: this.t('harvester.action.enableMaintenance'),
total: 1
};
const disableMaintenance = {
action: 'disableMaintenanceMode',
enabled: this.hasAction('disableMaintenanceMode'),
icon: 'icon icon-fw icon-lock',
label: this.t('harvester.action.disableMaintenance'),
total: 1
};
const shutDown = {
action: 'shutDown',
enabled: this.hasAction('powerActionPossible') && this.hasAction('powerAction') && !this.isStopped && !!this.inventory,
icon: 'icon icon-fw icon-dot',
label: this.t('harvester.action.shutdown'),
total: 1
};
const powerOn = {
action: 'powerOn',
enabled: this.hasAction('powerActionPossible') && this.hasAction('powerAction') && this.isStopped && !!this.inventory,
icon: 'icon icon-fw icon-play',
label: this.t('harvester.action.powerOn'),
total: 1
};
const reboot = {
action: 'reboot',
enabled: this.hasAction('powerActionPossible') && this.hasAction('powerAction') && !this.isStopped && !!this.inventory,
icon: 'icon icon-fw icon-refresh',
label: this.t('harvester.action.reboot'),
total: 1
};
return [
cordon,
uncordon,
enableMaintenance,
disableMaintenance,
shutDown,
powerOn,
reboot,
...super._availableActions
];
}
promptRemove(resources = this) {
this.$dispatch('promptModal', {
resources,
warningMessageKey: 'promptRemove.confirmRelatedResource',
component: 'ConfirmRelatedToRemoveDialog'
});
}
remove(resources = this) {
const nodes = Array.isArray(resources) ? resources : [resources];
nodes.forEach((node) => {
if (node.capiMachine) {
node.capiMachine.remove();
} else {
node.remove();
}
});
}
get capiMachine() {
const namespace = this.annotations?.[CAPI_ANNOTATIONS.CLUSTER_NAMESPACE];
const name = this.annotations?.[CAPI_ANNOTATIONS.MACHINE_NAME];
if (namespace && name) {
const inStore = this.$rootGetters['currentProduct'].inStore;
return this.$rootGetters[`${ inStore }/byId`](CAPI.MACHINE, `${ namespace }/${ name }`);
}
return null;
}
get confirmRemove() {
return true;
}
get consoleUrl() {
const url = this.metadata?.annotations?.[HCI_ANNOTATIONS.HOST_CONSOLE_URL];
if (!url) {
return false;
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
return `http://${ url }`;
}
return url;
}
get filteredSystemLabels() {
const reg = /(k3s|kubernetes|kubevirt|harvesterhci|k3os)+\.io/;
const labels = pickBy(this.labels, (value, key) => {
return !reg.test(key);
});
ALLOW_SYSTEM_LABEL_KEYS.map((key) => {
const value = this?.metadata?.labels?.[key];
if (value) {
labels[key] = value;
}
});
return labels;
}
get nameDisplay() {
return (
this.metadata?.annotations?.[HCI_ANNOTATIONS.HOST_CUSTOM_NAME] ||
this.name
);
}
get isKVMDisable() {
const allNotExist = !this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_AMD_CPU] && !this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_INTEL_CPU];
return this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_AMD_CPU] === 'false' || this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_INTEL_CPU] === 'false' || allNotExist;
}
get stateDisplay() {
if (this.isEnteringMaintenance) {
return 'Entering maintenance mode';
}
if (this.isStopping) {
return ucFirst(STATES_ENUM.STOPPING);
}
if (this.isStarting) {
return ucFirst(STATES_ENUM.STARTING);
}
if (this.isStopped) {
return ucFirst(STATES_ENUM.OFF);
}
if (this.isRebooting) {
return 'Rebooting';
}
if (this.isMaintenance) {
return 'Maintenance';
}
if (this.isCordoned) {
return 'Cordoned';
}
return stateDisplay(this.state);
}
get stateBackground() {
if (this.isStopped || this.isStopping || this.isStarting || this.isRebooting) {
return colorForState(
this.stateDisplay,
false,
this.stateObj?.transitioning
).replace('text-', 'bg-');
}
return colorForState(
this.stateDisplay,
this.stateObj?.error,
this.stateObj?.transitioning
).replace('text-', 'bg-');
}
get stateDescription() {
const currentIP = this.metadata?.annotations?.[HCI_ANNOTATIONS.CURRENT_IP];
const initIP = this.metadata?.annotations?.[HCI_ANNOTATIONS.INIT_IP];
if (initIP && currentIP && currentIP !== initIP) {
return this.t('harvester.host.inconsistentIP', { currentIP, initIP });
}
return super.stateDescription;
}
get stateObj() {
const currentIP = this.metadata?.annotations?.[HCI_ANNOTATIONS.CURRENT_IP];
const initIP = this.metadata?.annotations?.[HCI_ANNOTATIONS.INIT_IP];
if (initIP && currentIP && currentIP !== initIP) {
this.metadata.state.error = true;
}
return this.metadata?.state;
}
get detailLocation() {
const detailLocation = clone(this._detailLocation);
detailLocation.params.resource = HCI.HOST;
return detailLocation;
}
get doneOverride() {
const detailLocation = clone(this._detailLocation);
delete detailLocation.params.namespace;
delete detailLocation.params.id;
detailLocation.params.resource = HCI.HOST;
detailLocation.name = `${ HARVESTER_PRODUCT }-c-cluster-resource`;
return detailLocation;
}
get parentNameOverride() {
return this.$rootGetters['i18n/t'](`typeLabel."${ HCI.HOST }"`, { count: 1 })?.trim();
}
get parentLocationOverride() {
return this.doneOverride;
}
get internalIp() {
const addresses = this.status?.addresses || [];
return findLast(addresses, address => address.type === 'InternalIP')
?.address;
}
get isMaster() {
return (
this.metadata?.labels?.[HCI_ANNOTATIONS.NODE_ROLE_MASTER] !== undefined ||
this.metadata?.labels?.[HCI_ANNOTATIONS.NODE_ROLE_CONTROL_PLANE] !==
undefined
);
}
cordon() {
this.doActionGrowl('cordon', {});
}
uncordon() {
this.doAction('uncordon', {});
}
enableMaintenanceMode(resources = this) {
this.$dispatch('promptModal', {
resources,
component: 'HarvesterMaintenanceDialog'
});
}
disableMaintenanceMode() {
this.doAction('disableMaintenanceMode', {});
}
get isUnSchedulable() {
return (
this.metadata?.labels?.[HCI_ANNOTATIONS.NODE_SCHEDULABLE] === 'false' ||
this.spec.unschedulable
);
}
get isMigratable() {
const states = ['in-progress', 'unavailable'];
return (
!this.metadata?.annotations?.[HCI_ANNOTATIONS.MAINTENANCE_STATUS] &&
!this.isUnSchedulable &&
!states.includes(this.state)
);
}
get isCordoned() {
return (this.isUnSchedulable && !this.isEtcd) || this.hasAction('uncordon');
}
get isEtcd() {
return this.metadata?.labels?.[HCI_ANNOTATIONS.NODE_ROLE_ETCD];
}
get isEnteringMaintenance() {
return (
this.metadata?.annotations?.[HCI_ANNOTATIONS.MAINTENANCE_STATUS] ===
'running'
);
}
get isMaintenance() {
return (
this.metadata?.annotations?.[HCI_ANNOTATIONS.MAINTENANCE_STATUS] ===
'completed'
);
}
get longhornDisks() {
const inStore = this.$rootGetters['currentProduct'].inStore;
const longhornNode = this.$rootGetters[`${ inStore }/byId`](
LONGHORN.NODES,
`longhorn-system/${ this.id }`
);
const diskStatus = longhornNode?.status?.diskStatus || {};
const diskSpec = longhornNode?.spec?.disks || {};
const longhornDisks = Object.keys(diskStatus).map((key) => {
const conditions = diskStatus[key]?.conditions || [];
let readyCondition = {};
let schedulableCondition = {};
if (isArray(conditions)) {
readyCondition = findBy(conditions, 'type', 'Ready') || {};
schedulableCondition = findBy(conditions, 'type', 'Schedulable') || {};
} else {
readyCondition = conditions.Ready;
schedulableCondition = conditions.Schedulable;
}
let state;
if (readyCondition?.status !== 'True' || schedulableCondition?.status !== 'True') {
state = WARNING;
} else {
state = HEALTHY;
}
return {
...diskSpec[key],
...diskStatus[key],
name: key,
storageReserved: diskSpec[key]?.storageReserved,
storageAvailable: diskStatus[key]?.storageAvailable,
storageMaximum: diskStatus[key]?.storageMaximum,
storageScheduled: diskStatus[key]?.storageScheduled,
readyCondition,
schedulableCondition,
state,
};
});
return longhornDisks;
}
get pods() {
const inStore = this.$rootGetters['currentProduct'].inStore;
const pods = this.$rootGetters[`${ inStore }/all`](POD) || [];
return pods.filter(
p => p?.spec?.nodeName === this.id && p?.metadata?.name !== 'removing'
);
}
get reserved() {
try {
return JSON.parse(this.metadata.annotations[HCI_ANNOTATIONS.HOST_REQUEST] || '{}');
} catch {
return {};
}
}
get cpuReserved() {
return parseSi(this.reserved.cpu || '0');
}
get memoryReserved() {
return parseSi(this.reserved.memory || '0');
}
get canDelete() {
const nodes = this.$rootGetters['harvester/all'](NODE) || [];
return nodes.length > 1;
}
get vlanStatuses() {
const inStore = this.$rootGetters['currentProduct'].inStore;
const nodeId = this.id;
const vlanStatuses = this.$rootGetters[`${ inStore }/all`](HCI.VLAN_STATUS);
return vlanStatuses.filter(s => s?.status?.node === nodeId) || [];
}
get blockDevices() {
const inStore = this.$rootGetters['currentProduct'].inStore;
const nodeId = this.id;
const blockDevices = this.$rootGetters[`${ inStore }/all`](HCI.BLOCK_DEVICE);
return blockDevices.filter(s => s?.spec?.nodeName === nodeId) || [];
}
get unProvisionedDisks() {
const blockDevices = this.blockDevices || [];
return blockDevices.filter(d => d?.spec?.fileSystem?.provisioned && d?.status?.provisionPhase !== 'Provisioned');
}
get diskStatusCount() {
const errorBlockDevices = this.unProvisionedDisks.filter(b => b.metadata.state.error) || [];
let errorCount = 0;
this.longhornDisks.map((d) => {
if (d.state === 'warning') {
errorCount++;
}
});
const total = this.longhornDisks.length + errorBlockDevices.length;
return {
total,
errorCount: errorCount + errorBlockDevices.length,
useful: total - errorCount,
};
}
get manufacturer() {
return this.metadata?.labels?.[HCI_ANNOTATIONS.NODE_MANUFACTURER];
}
get serialNumber() {
return this.metadata?.labels?.[HCI_ANNOTATIONS.NODE_SERIAL_NUMBER];
}
get model() {
return this.metadata?.labels?.[HCI_ANNOTATIONS.NODE_MODEL];
}
get isStopped() {
const inventory = this.inventory || {};
return inventory.spec?.powerActionRequested === 'shutdown' &&
inventory.status?.powerAction?.actionStatus === 'complete';
}
get isStopping() {
const inventory = this.inventory || {};
if (!Object.prototype.hasOwnProperty.call(inventory?.status?.powerAction || {}, 'actionStatus')) {
return inventory.spec?.powerActionRequested === 'shutdown';
} else {
return false;
}
}
get isStarted() {
const inventory = this.inventory || {};
return inventory.spec?.powerActionRequested === 'poweron' &&
inventory.status?.powerAction?.actionStatus === 'complete';
}
get isStarting() {
const inventory = this.inventory || {};
if (!Object.prototype.hasOwnProperty.call(inventory?.status?.powerAction || {}, 'actionStatus')) {
return inventory.spec?.powerActionRequested === 'poweron';
} else {
return false;
}
}
get isRebooting() {
const inventory = this.inventory || {};
if (!Object.prototype.hasOwnProperty.call(inventory?.status?.powerAction || {}, 'actionStatus')) {
return inventory.spec?.powerActionRequested === 'reboot';
} else {
return false;
}
}
async shutDown(resources = this) {
try {
await this.doAction('powerActionPossible', {});
await this.doAction('powerAction', { operation: 'shutdown' });
await this.$dispatch('growl/success', {
title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.host.powerAction.message.success', {
name: this.name,
operation: 'shut down'
})
}, { root: true });
} catch (err) {
await this.$dispatch('growl/error', {
title: this.t('generic.notification.title.error'),
message: err,
}, { root: true });
}
}
async powerOn(resources = this) {
const operation = 'poweron';
try {
await this.doAction('powerActionPossible', {});
await this.doAction('powerAction', { operation });
await this.$dispatch('growl/success', {
title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.host.powerAction.message.success', {
name: this.name,
operation,
})
}, { root: true });
} catch (err) {
await this.$dispatch('growl/error', {
title: this.t('generic.notification.title.error'),
message: err,
}, { root: true });
}
}
async reboot(resources = this) {
const operation = 'reboot';
try {
await this.doAction('powerActionPossible', {});
await this.doAction('powerAction', { operation });
await this.$dispatch('growl/success', {
title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.host.powerAction.message.success', {
name: this.name,
operation,
})
}, { root: true });
} catch (err) {
await this.$dispatch('growl/error', {
title: this.t('generic.notification.title.error'),
message: err,
}, { root: true });
}
}
get inventory() {
const inStore = this.$rootGetters['currentProduct'].inStore;
const inventories = this.$rootGetters[`${ inStore }/all`](HCI.INVENTORY) || [];
return inventories.find(inv => inv.id === `harvester-system/${ this.id }`);
}
get warningMessages() {
let out = [];
out = out.concat(this.inventory?.warningMessages || []);
return out;
}
}
@@ -0,0 +1,307 @@
import Vue from 'vue';
import { _CLONE } from '@shell/config/query-params';
import pick from 'lodash/pick';
import { HCI, VOLUME_SNAPSHOT } from '../../types';
import { PV, LONGHORN } from '@shell/config/types';
import { DESCRIPTION } from '@shell/config/labels-annotations';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { findBy } from '@shell/utils/array';
import { get, clone } from '@shell/utils/object';
import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
import HarvesterResource from '../harvester';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
const DEGRADED_ERROR = 'replica scheduling failed';
export default class HciPv extends HarvesterResource {
applyDefaults(_, realMode) {
const accessModes = realMode === _CLONE ? this.spec.accessModes : [];
const storage =
realMode === _CLONE ? this.spec.resources.requests.storage : null;
const storageClassName =
realMode === _CLONE ? this.spec.storageClassName : '';
Vue.set(this, 'spec', {
accessModes,
storageClassName,
volumeName: '',
resources: { requests: { storage } }
});
}
get availableActions() {
const out = super._availableActions;
const clone = out.find(action => action.action === 'goToClone');
if (clone) {
clone.action = 'goToCloneVolume';
}
return [
{
action: 'exportImage',
enabled: this.hasAction('export'),
icon: 'icon icon-copy',
label: this.t('harvester.action.exportImage')
},
{
action: 'cancelExpand',
enabled: this.hasAction('cancelExpand'),
icon: 'icon icon-backup',
label: this.t('harvester.action.cancelExpand')
},
{
action: 'snapshot',
enabled: this.hasAction('snapshot'),
icon: 'icon icon-backup',
label: this.t('harvester.action.snapshot'),
},
...out
];
}
exportImage(resources = this) {
this.$dispatch('promptModal', {
resources,
component: 'HarvesterExportImageDialog'
});
}
cancelExpand(resources = this) {
this.doActionGrowl('cancelExpand', {});
}
snapshot(resources = this) {
this.$dispatch('promptModal', {
resources,
component: 'SnapshotDialog'
});
}
goToCloneVolume(resources = this) {
this.$dispatch('promptModal', {
resources,
component: 'VolumeCloneDialog'
});
}
cleanForNew() {
this.$dispatch(`cleanForNew`, this);
delete this.metadata.finalizers;
const keys = [HCI_ANNOTATIONS.IMAGE_ID, DESCRIPTION];
this.metadata.annotations = pick(this.metadata.annotations, keys);
}
get canUpdate() {
return this.hasLink('update');
}
get stateDisplay() {
const ownedBy = this?.metadata?.annotations?.[HCI_ANNOTATIONS.OWNED_BY];
const volumeError = this.relatedPV?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_ERROR];
const degradedVolume = volumeError === DEGRADED_ERROR;
const status = this?.status?.phase === 'Bound' && !volumeError && this.isLonghornVolumeReady ? 'Ready' : 'Not Ready';
const conditions = this?.status?.conditions || [];
if (findBy(conditions, 'type', 'Resizing')?.status === 'True') {
return 'Resizing';
} else if (ownedBy && !volumeError) {
return 'In-use';
} else if (degradedVolume) {
return 'Degraded';
} else {
return status;
}
}
// state is similar with stateDisplay, the reason we keep this property is the status of In-use should not be displayed on vm detail page
get state() {
const volumeError = this.relatedPV?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_ERROR];
const degradedVolume = volumeError === DEGRADED_ERROR;
let status = this?.status?.phase === 'Bound' && !volumeError ? 'Ready' : 'Not Ready';
const conditions = this?.status?.conditions || [];
if (degradedVolume) {
status = 'Degraded';
}
if (findBy(conditions, 'type', 'Resizing')?.status === 'True') {
status = 'Resizing';
}
return status;
}
get stateColor() {
const state = this.stateDisplay;
return colorForState(state);
}
get stateDescription() {
return (
super.stateDescription
);
}
get detailLocation() {
const detailLocation = clone(this._detailLocation);
detailLocation.params.resource = HCI.VOLUME;
return detailLocation;
}
get doneOverride() {
const detailLocation = clone(this._detailLocation);
delete detailLocation.params.namespace;
delete detailLocation.params.id;
detailLocation.params.resource = HCI.VOLUME;
detailLocation.name = `${ HARVESTER_PRODUCT }-c-cluster-resource`;
return detailLocation;
}
get parentNameOverride() {
return this.$rootGetters['i18n/t'](`typeLabel."${ HCI.VOLUME }"`, { count: 1 }).trim();
}
get parentLocationOverride() {
return this.doneOverride;
}
get phaseState() {
return this.status?.phase || 'N/A';
}
get attachVM() {
const allVMs = this.$rootGetters['harvester/all'](HCI.VM);
const ownedBy =
get(this, `metadata.annotations."${ HCI_ANNOTATIONS.OWNED_BY }"`) || '';
if (!ownedBy) {
return null;
}
const ownedId = JSON.parse(ownedBy)[0]?.refs?.[0];
return allVMs.find(D => D.id === ownedId);
}
get isAvailable() {
const unAvailable = ['Resizing', 'Not Ready'];
return !unAvailable.includes(this.stateDisplay);
}
get volumeSort() {
const volume = this.spec?.resources?.requests?.storage || 0;
return parseInt(volume);
}
get isSystemResource() {
const systemNamespaces = this.$rootGetters['systemNamespaces'];
if (systemNamespaces.includes(this.metadata?.namespace)) {
return true;
}
return false;
}
get longhornVolume() {
const inStore = this.$rootGetters['currentProduct'].inStore;
return this.$rootGetters[`${ inStore }/all`](LONGHORN.VOLUMES).find(v => v.metadata?.name === this.spec?.volumeName);
}
get longhornEngine() {
const inStore = this.$rootGetters['currentProduct'].inStore;
return this.$rootGetters[`${ inStore }/all`](LONGHORN.ENGINES).find(v => v.spec?.volumeName === this.spec?.volumeName);
}
// https://github.com/longhorn/longhorn-manager/blob/master/api/model.go#L1151
get isLonghornVolumeReady() {
let ready = true;
const longhornVolume = this.longhornVolume || {};
const scheduledCondition = (longhornVolume?.status?.conditions || []).find(c => c.type === 'Scheduled' || c.type === 'scheduled') || {};
if ((longhornVolume?.spec?.nodeID === '' && longhornVolume?.status?.state !== 'detached') ||
(longhornVolume?.status?.state === 'detached' && scheduledCondition.status !== 'True') ||
longhornVolume?.status?.robustness === 'faulted' ||
longhornVolume?.status?.restoreRequired ||
longhornVolume?.status?.cloneStatus?.state === 'failed'
) {
ready = false;
}
return ready;
}
get relatedVolumeSnapshotCounts() {
const snapshots = this.$rootGetters['harvester/all'](VOLUME_SNAPSHOT);
return snapshots.filter((snapshot) => {
const volumeId = `${ snapshot.metadata?.namespace }/${ snapshot.spec?.source?.persistentVolumeClaimName }`;
const kind = snapshot.metadata?.ownerReferences?.[0]?.kind;
return volumeId === this.id && kind === 'PersistentVolumeClaim';
});
}
get originalSnapshot() {
if (this.spec?.dataSource) {
return this.$rootGetters['harvester/all'](VOLUME_SNAPSHOT).find(V => V.metadata?.name === this.spec.dataSource.name);
} else {
return null;
}
}
get source() {
const imageId = get(this, `metadata.annotations."${ HCI_ANNOTATIONS.IMAGE_ID }"`);
return imageId ? 'image' : 'data';
}
get warnDeletionMessage() {
return this.t('harvester.volume.promptRemove.tips');
}
get relatedPV() {
return this.$rootGetters['harvester/all'](PV).find(pv => pv.metadata?.name === this.spec?.volumeName);
}
get resourceExternalLink() {
const host = window.location.host;
const { params } = this.currentRoute();
const volumeName = this.spec?.volumeName;
if (!volumeName) {
return null;
}
return {
tipsKey: 'harvester.volume.externalLink.tips',
url: `https://${ host }/k8s/clusters/${ params.cluster }/api/v1/namespaces/longhorn-system/services/http:longhorn-frontend:80/proxy/#/volume/${ volumeName }`
};
}
get customValidationRules() {
return [
{
nullable: false,
path: 'spec.resources.requests.storage',
required: true,
validators: ['volumeSize']
},
];
}
}
+185
View File
@@ -0,0 +1,185 @@
import includes from 'lodash/includes';
import { findBy } from '@shell/utils/array';
import { get } from '@shell/utils/object';
import { NODE } from '@shell/config/types';
import HarvesterResource from '../harvester';
const POD_STATUS_NOT_SCHEDULABLE = 'POD_NOT_SCHEDULABLE';
const POD_STATUS_FAILED = 'POD_FAILED';
const POD_STATUS_CRASHLOOP_BACKOFF = 'POD_CRASHLOOP_BACKOFF';
const POD_STATUS_UNKNOWN = 'POD_STATUS_UNKNOWN';
const POD_STATUS_CONTAINER_FAILING = 'POD_CONTAINER_FAILING';
const POD_STATUS_NOT_READY = 'POD_NOT_READY';
const POD_STATUS_PENDING = 'POD_STATUS_PENDING';
const POD_STATUS_COMPLETED = 'POD_STATUS_COMPLETED';
const POD_STATUS_SUCCEEDED = 'POD_STATUS_SUCCEEDED';
const POD_STATUS_RUNNING = 'POD_STATUS_RUNNING';
const failedWaitingContainerReasons = ['ImagePullBackOff', 'ErrImagePull', 'CrashLoopBackOff'];
const failedTerminationContainerReasons = ['Error'];
const errorStatusMapper = {
Failed: POD_STATUS_FAILED,
CrashLoopBackOff: POD_STATUS_CRASHLOOP_BACKOFF,
Unknown: POD_STATUS_UNKNOWN,
};
const okStatusMapper = {
Pending: POD_STATUS_PENDING,
Running: POD_STATUS_RUNNING,
Completed: POD_STATUS_COMPLETED,
Succeeded: POD_STATUS_SUCCEEDED,
};
const stateReasonResolver = {
terminated: ({ reason, exitCode }) => `Terminated with ${ reason }${ exitCode ? ` (exit code ${ exitCode }).` : '.' }`,
waiting: ({ reason }) => `Waiting (${ reason }).`,
};
export default class HciPod extends HarvesterResource {
get inStore() {
return this.$rootGetters['currentProduct'].inStore;
}
get nodes() {
return this.$rootGetters[`${ this.inStore }/all`](NODE);
}
get node() {
const { nodeName } = this.spec;
return this.nodes.filter((node) => {
return node?.metadata?.name === nodeName;
})[0];
}
get getPodStatus() {
return this.isNotSchedulable ||
this.hasErrorStatus ||
this.isContainerFailing ||
this.isNotReady ||
this.hasOkStatus || { status: POD_STATUS_UNKNOWN };
}
get isNotSchedulable() {
if (!this.isPodSchedulable) {
const conditions = get(this, 'status.conditions');
const podScheduledCond = findBy(conditions, 'type', 'PodScheduled');
return {
status: POD_STATUS_NOT_SCHEDULABLE,
message: podScheduledCond.message || 'Pod scheduling failed.',
};
}
return null;
}
get hasErrorStatus() {
const status = errorStatusMapper[this?.status?.phase];
if (status) {
return {
status,
message: this.getContainerStatusReason(this.findFailingContainerStatus),
};
}
return null;
}
get isPodSchedulable() {
const conditions = get(this, 'status.conditions');
const podScheduledCond = findBy(conditions, 'type', 'PodScheduled');
return !(
podScheduledCond &&
podScheduledCond.status !== 'True' &&
podScheduledCond.reason === 'Unschedulable'
);
}
get findFailingContainerStatus() {
return (get(this, 'status.containerStatuses'), []).find((container) => {
return !container.ready &&
(includes(failedWaitingContainerReasons, get(container, 'state.waiting.reason')) ||
includes(failedTerminationContainerReasons, get(container, 'state.terminated.reason')));
});
}
get getContainerStatusReason() {
return (containerStatus) => {
if (containerStatus) {
const stateName = Object.getOwnPropertyNames(containerStatus.state).find(
pn => !!containerStatus.state[pn].reason,
);
if (stateName) {
const state = containerStatus.state[stateName];
return (
state.message ||
(stateReasonResolver[stateName] && stateReasonResolver[stateName](state)) ||
stateName
);
}
}
return undefined;
};
}
get isContainerFailing() {
const failingContainer = this.findFailingContainerStatus;
if (failingContainer) {
return {
status: POD_STATUS_CONTAINER_FAILING,
message: this.getContainerStatusReason(failingContainer),
};
}
return null;
}
get isNotReady() {
const message = this.findPodFalseStatusConditionMessage;
if (message) {
return {
status: POD_STATUS_NOT_READY,
message,
};
}
return null;
}
get hasOkStatus() {
const status = okStatusMapper[this?.status?.phase];
if (status) {
return { status };
}
return null;
}
get findPodFalseStatusConditionMessage() {
const notReadyConditions = this.getPodFalseStatusConditions;
if (notReadyConditions.length > 0) {
return notReadyConditions[0].message || `Step: ${ notReadyConditions[0].type }`;
}
return undefined;
}
get getPodFalseStatusConditions() {
const conditions = get(this, 'status.conditions') || [];
return conditions.filter(condition => condition.status !== 'True');
}
}
+88
View File
@@ -0,0 +1,88 @@
import { clone } from '@shell/utils/object';
import { HCI } from '../../types';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
import Secret from '@shell/models/secret';
export default class HciSecret extends Secret {
get _detailLocation() {
const schema = this.$getters['schemaFor'](this.type);
const id = this.id?.replace(/.*\//, '');
return {
name: `${ HARVESTER_PRODUCT }-c-cluster-resource${ schema?.attributes?.namespaced ? '-namespace' : '' }-id`,
params: {
product: HARVESTER_PRODUCT,
cluster: this.$rootGetters['clusterId'],
resource: this.type,
id,
namespace: this.metadata.namespace,
},
};
}
get detailLocation() {
const detailLocation = clone(this._detailLocation);
detailLocation.params.resource = HCI.SECRET;
return detailLocation;
}
get doneOverride() {
const detailLocation = clone(this._detailLocation);
delete detailLocation.params.namespace;
delete detailLocation.params.id;
detailLocation.params.resource = HCI.SECRET;
detailLocation.name = `${ HARVESTER_PRODUCT }-c-cluster-resource`;
return detailLocation;
}
get doneRoute() {
return this.doneOverride.name;
}
get parentNameOverride() {
return this.$rootGetters['i18n/t'](`typeLabel."${ HCI.SECRET }"`, { count: 1 })?.trim();
}
get parentLocationOverride() {
return this.doneOverride;
}
get details() {
const out = [
{
label: this.t('secret.type'),
content: this.typeDisplay
}
];
if (this.cn) {
out.push({
label: this.t('secret.certificate.cn'),
content: this.plusMoreNames ? `${ this.cn } ${ this.t('secret.certificate.plusMore', { n: this.plusMoreNames }) }` : this.cn
});
}
if (this.issuer) {
out.push({
label: this.t('secret.certificate.issuer'),
content: this.issuer
});
}
if (this.notAfter) {
out.push({
label: 'Expires',
formatter: 'Date',
formatterOpts: { class: this.dateClass },
content: this.notAfter
});
}
return out;
}
}
@@ -0,0 +1,70 @@
import { clone } from '@shell/utils/object';
import { HCI } from '../../types';
import HarvesterResource from '../harvester';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
import { PVC } from '@shell/config/types';
export default class HciSnapshot extends HarvesterResource {
get availableActions() {
return [
{
action: 'restoreSnapshot',
enabled: this.hasAction('restore'),
icon: 'icon icon-refresh',
label: this.t('harvester.action.restore'),
},
...super._availableActions
];
}
get canClone() {
return false;
}
restoreSnapshot(resources = this) {
this.$dispatch('promptModal', {
resources,
component: 'RestoreSnapshotDialog'
});
}
get detailLocation() {
const detailLocation = clone(this._detailLocation);
detailLocation.params.resource = HCI.SNAPSHOT;
return detailLocation;
}
get doneOverride() {
const detailLocation = clone(this._detailLocation);
delete detailLocation.params.namespace;
delete detailLocation.params.id;
detailLocation.params.resource = HCI.SNAPSHOT;
detailLocation.name = `${ HARVESTER_PRODUCT }-c-cluster-resource`;
return detailLocation;
}
get parentNameOverride() {
return this.$rootGetters['i18n/t'](`typeLabel."${ HCI.SNAPSHOT }"`, { count: 1 })?.trim();
}
get parentLocationOverride() {
return this.doneOverride;
}
get volume() {
const inStore = this.$rootGetters['currentProduct'].inStore;
const namespace = this?.metadata?.namespace;
const pvc = this?.spec?.source?.persistentVolumeClaimName;
const volume = this.$rootGetters[`${ inStore }/byId`](
PVC,
`${ namespace }/${ pvc }`
);
return volume;
}
}
@@ -0,0 +1,34 @@
import { clone } from '@shell/utils/object';
import { HCI } from '../../types';
import StorageClass from '@shell/models/storage.k8s.io.storageclass';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
export default class HciStorageClass extends StorageClass {
get detailLocation() {
const detailLocation = clone(this._detailLocation);
detailLocation.params.resource = HCI.STORAGE;
detailLocation.name = `${ HARVESTER_PRODUCT }-c-cluster-resource-id`;
return detailLocation;
}
get doneOverride() {
const detailLocation = clone(this._detailLocation);
delete detailLocation.params.namespace;
delete detailLocation.params.id;
detailLocation.params.resource = HCI.STORAGE;
detailLocation.name = `${ HARVESTER_PRODUCT }-c-cluster-resource`;
return detailLocation;
}
get parentLocationOverride() {
return this.doneOverride;
}
get parentNameOverride() {
return this.$rootGetters['i18n/t'](`typeLabel."${ HCI.STORAGE }"`, { count: 1 })?.trim();
}
}
@@ -0,0 +1,140 @@
import HarvesterResource from './harvester';
import { HCI as HCI_ANNOTATIONS } from '../config/labels-annotations';
import jsyaml from 'js-yaml';
import startCase from 'lodash/startCase';
export default class HciAddonConfig extends HarvesterResource {
get availableActions() {
const out = super._availableActions;
if (this.id === 'harvester-system/rancher-vcluster') {
const rancherDashboard = {
action: 'goToRancher',
enabled: this.spec.enabled,
icon: 'icon icon-external-link',
label: this.t('harvester.addons.rancherVcluster.accessRancher'),
};
out.push(rancherDashboard);
}
const toggleAddon = {
action: 'toggleAddon',
enabled: true,
icon: this.spec.enabled ? 'icon icon-pause' : 'icon icon-play',
label: this.spec.enabled ? this.t('generic.disable') : this.t('generic.enable'),
};
out.unshift(toggleAddon);
return out;
}
async toggleAddon() {
const enableHistory = this.spec.enabled;
try {
if (!this.spec.enabled && this.id === 'rancher-vcluster/rancher-vcluster') {
const valuesContent = jsyaml.load(this.spec.valuesContent);
if (!valuesContent.hostname || !valuesContent.bootstrapPassword) {
this.goToEdit();
return;
}
}
this.spec.enabled = !this.spec.enabled;
await this.save();
} catch (err) {
this.spec.enabled = enableHistory;
this.$dispatch('growl/fromError', {
title: this.t('harvester.addons.switchFailed', { action: enableHistory ? this.t('generic.disable') : this.t('generic.enable'), name: (this.metadata.name) }),
err,
}, { root: true });
}
}
goToRancher() {
const valuesContent = jsyaml.load(this.spec.valuesContent);
window.open(
`https://${ valuesContent.hostname }`,
'_blank',
);
}
get rancherHostname() {
const valuesContent = jsyaml.load(this.spec.valuesContent);
return `https://${ valuesContent.hostname }`;
}
get stateColor() {
const state = this.stateDisplay;
if (state?.toLowerCase().includes('enabled') || state?.toLowerCase().includes('success')) {
return 'text-success';
} else if (state === 'Disabled') {
return 'text-darker';
} else if (state?.toLowerCase().includes('ing')) {
return 'text-info';
} else if (state?.toLowerCase().includes('failed') || state?.toLowerCase().includes('error')) {
return 'text-error';
} else {
return 'text-info';
}
}
get stateDisplay() {
const out = this?.status?.status;
if (!out) {
return 'Disabled';
}
if (out.startsWith('Addon')) {
return startCase(out.replace('Addon', ''));
}
return out;
}
get stateDescription() {
const failedCondition = (this.status?.conditions || []).find(C => C.type === 'OperationFailed');
return failedCondition?.message || super.stateDescription;
}
get displayName() {
const isExperimental = this.metadata?.labels?.[HCI_ANNOTATIONS.ADDON_EXPERIMENTAL] === 'true';
return isExperimental ? `${ this.metadata.name } (${ this.t('generic.experimental') })` : this.metadata.name;
}
get customValidationRules() {
let rules = [];
if (this.metadata.name === 'rancher-monitoring') {
rules = [
{
nullable: false,
path: 'spec.valuesContent',
validators: ['rancherMonitoring'],
},
];
}
if (this.metadata.name === 'rancher-logging') {
rules = [
{
nullable: false,
path: 'spec.valuesContent',
validators: ['rancherLogging'],
},
];
}
return rules;
}
}
@@ -0,0 +1,62 @@
import { HCI } from '../types';
import HarvesterResource from './harvester';
export default class HciBlockDevice extends HarvesterResource {
get childParts() {
const inStore = this.$rootGetters['currentProduct'].inStore;
const bds = this.$rootGetters[`${ inStore }/all`](HCI.BLOCK_DEVICE);
const parts = bds.filter((b) => {
return b.status?.deviceStatus?.parentDevice === this.spec?.devPath &&
b.spec.nodeName === this.spec.nodeName;
});
return parts;
}
get isChildPartProvisioned() {
const parts = this.childParts.filter(p => p.spec?.fileSystem?.provisioned) || [];
return parts.length > 0;
}
get provisionPhase() {
const label = this?.status?.provisionPhase;
let color = '';
let icon = '';
switch (label) {
case 'Provisioned':
color = 'bg-success';
icon = 'icon-checkmark';
break;
case 'Unprovisioning':
color = 'bg-warning';
icon = 'icon-warning';
break;
case 'NotProvisioned':
color = 'bg-error';
icon = 'icon-warning';
break;
default:
break;
}
return {
label,
color,
icon,
};
}
get displayName() {
return this.status?.deviceStatus?.devPath || this?.metadata?.name;
}
get isFormatting() {
const conditions = this?.status?.conditions || [];
const formatting = conditions.find(c => c.type === 'Formatting') || {};
return formatting.status === 'True';
}
}
@@ -0,0 +1,12 @@
import { get } from '@shell/utils/object';
import { findBy } from '@shell/utils/array';
import HarvesterResource from './harvester';
export default class HciKeypair extends HarvesterResource {
get stateDisplay() {
const conditions = get(this, 'status.conditions');
const status = (findBy(conditions, 'type', 'validated') || {}).status ;
return status === 'True' ? 'Validated' : 'Not Validated';
}
}
@@ -0,0 +1,158 @@
import { findBy } from '@shell/utils/array';
import { HCI } from '../types';
import { HCI_ALLOWED_SETTINGS, HCI_SETTING } from '../config/settings';
import HarvesterResource from './harvester';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
export default class HciSetting extends HarvesterResource {
get _availableActions() {
const toFilter = ['cloneYaml', 'download', 'goToEditYaml', 'goToViewYaml', 'goToViewConfig', 'promptRemove'];
const settingMetadata = HCI_ALLOWED_SETTINGS[this.id];
let out = super._availableActions;
// Some settings are not editable
if ( settingMetadata?.readOnly || this.fromEnv ) {
toFilter.push('goToEdit');
}
out = out.filter((action) => {
return (!toFilter.includes(action.action));
});
// Change the label on the first action (edit)
const editAction = out.find(action => action.action === 'goToEdit');
if (editAction) {
editAction.label = this.t('advancedSettings.edit.label');
}
const schema = this.$getters['schemaFor'](HCI.UPGRADE);
const hasUpgradeAccess = !!schema?.collectionMethods.find(x => ['post'].includes(x.toLowerCase()));
if (this.id === HCI_SETTING.SERVER_VERSION && hasUpgradeAccess) {
const latestUpgrade = this.$getters['all'](HCI.UPGRADE).find(upgrade => upgrade.isLatestUpgrade);
out.unshift({
action: 'goToAirgapUpgrade',
enabled: true,
icon: 'icon icon-refresh',
label: this.t('harvester.upgradePage.upgrade'),
disabled: !!latestUpgrade && !latestUpgrade?.isUpgradeSucceeded
});
}
return out;
}
goToAirgapUpgrade() {
const router = this.currentRouter();
router.push({
name: `${ HARVESTER_PRODUCT }-c-cluster-airgapupgrade`,
params: { cluster: this.$rootGetters['currentCluster'].id, product: 'harvester' },
});
}
get customValue() {
if (this.metadata.name === HCI_SETTING.STORAGE_NETWORK) {
try {
return JSON.stringify(JSON.parse(this.value), null, 2);
} catch (e) {}
}
return false;
}
get customFormatter() {
if (this.metadata.name === HCI_SETTING.STORAGE_NETWORK) {
try {
JSON.stringify(JSON.parse(this.value), null, 2);
return 'json';
} catch (e) {
}
}
return false;
}
get backupTargetIsEmpty() {
return !this.value;
}
get errMessage() {
const configuredCondition = findBy((this?.status?.conditions || []), 'type', 'configured') || {};
if (this.metadata?.state?.error === true) {
return this.metadata.state.message;
} else if (configuredCondition?.status === 'False') {
return configuredCondition.message;
} else {
return false;
}
}
get valueOrDefaultValue() {
return this.value || this.default;
}
get currentVersion() {
return this.value || '';
}
get displayValue() { // Select the field you want to display
if (this.id === 'backup-target') {
return this.parseValue?.endpoint || ' ';
}
return null;
}
get parseValue() {
let parseDefaultValue = {};
try {
parseDefaultValue = JSON.parse(this.value);
} catch (err) {
parseDefaultValue = JSON.parse(this.default);
}
return parseDefaultValue;
}
get isS3() {
return this.parseValue.type === 's3';
}
get isNFS() {
return this.parseValue.type === 'nfs';
}
get customValidationRules() {
const id = this.id;
const out = [];
switch (id) {
case 'backup-target':
out.push( {
nullable: false,
path: 'value',
type: 'string',
validators: ['backupTarget'],
});
break;
case 'ntp-servers':
out.push( {
nullable: true,
path: 'value',
validators: ['ntpServers'],
});
break;
}
return out;
}
}
@@ -0,0 +1,35 @@
import HarvesterResource from './harvester';
export default class HciSupportBundle extends HarvesterResource {
get bundleState() {
const state = this?.status?.state;
// ready、generating
return state;
}
get bundleMessage() {
const state = this?.metadata?.state;
if (state.error) {
return state?.message;
}
return false;
}
get precent() {
return this?.status?.progress / 100 || 0;
}
get customValidationRules() {
return [
{
nullable: false,
path: 'spec.description',
required: true,
translationKey: 'harvester.modal.bundle.description',
},
];
}
}
@@ -0,0 +1,226 @@
import jsyaml from 'js-yaml';
import { NODE } from '@shell/config/types';
import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
import { HCI } from '@pkg/harvester/config/labels-annotations';
import HarvesterResource from './harvester';
export default class HciUpgrade extends HarvesterResource {
get isLatestUpgrade() {
return this?.metadata?.labels?.[HCI.LATEST_UPGRADE] === 'true';
}
get isUpgradeSucceeded() {
return this?.metadata?.labels?.[HCI.UPGRADE_STATE] === 'Succeeded';
}
get hasReadMessage() {
return this?.metadata?.labels?.[HCI.REAY_MESSAGE] === 'true';
}
get repoInfo() {
const repoInfo = this?.status?.repoInfo;
if (repoInfo) {
try {
return jsyaml.load(repoInfo);
} catch (e) {
return false;
}
}
return false;
}
get stateDisplay() {
const conditions = this?.status?.conditions || [];
const completedCondition = conditions.find( cond => cond.type === 'Completed');
const status = completedCondition?.status;
if (status === 'True') {
return 'Success';
} else if (status === 'False') {
return 'Fail';
} else {
return 'on-going';
}
}
get stateColor() {
return colorForState(this.stateDisplay);
}
get nodes() {
return this.$rootGetters['harvester/all'](NODE);
}
get upgradeImage() {
return this?.status?.imageID;
}
get upgradeMessage() {
const upgradeMessage = [];
const nodeStatuses = this?.status?.nodeStatuses || {};
const conditions = this?.status?.conditions || [];
for (const key in nodeStatuses) {
const state = nodeStatuses[key]?.state;
if (nodeStatuses[key] && state !== 'Succeeded' && state !== 'succeeded') {
upgradeMessage.push({
id: key,
message: `The node ${ key } is ${ nodeStatuses[key]?.state }`
});
}
}
for (let i = 0; i < conditions.length; i++) {
const type = conditions[i].type;
if (type === 'systemServiceUpgraded' && conditions[i]?.status !== 'True') {
upgradeMessage.push({
id: 'systemService',
message: `The systemService is upgrading`
});
}
}
if (this.metadata?.state?.message && this.metadata?.state?.error) {
upgradeMessage.push({
id: 'message',
message: `${ this.metadata.state.message }`
});
}
return upgradeMessage;
}
get createRepo() {
const conditions = this?.status?.conditions || [];
const repoCondition = conditions.find( cond => cond.type === 'RepoReady');
const isReady = repoCondition?.status === 'True';
return {
isReady,
message: repoCondition?.message || repoCondition?.reason
};
}
get overallMessage() {
const conditions = this?.status?.conditions || [];
const completedCondition = conditions.find( cond => cond.type === 'Completed');
const hasError = completedCondition?.status === 'False';
const message = completedCondition?.message || completedCondition?.reason;
return hasError ? message : '';
}
get upgradeImageMessage() {
const conditions = this?.status?.conditions || [];
const imageReady = conditions.find( cond => cond.type === 'ImageReady');
const hasError = imageReady?.status === 'False';
const message = imageReady?.message || imageReady?.reason;
return hasError ? message : '';
}
get nodeUpgradeMessage() {
const message = [];
const nodeStatuses = this?.status?.nodeStatuses || {};
for (const key in nodeStatuses) {
const state = nodeStatuses[key]?.state;
const _message = nodeStatuses[key]?.message;
let percent = 0;
if (state === 'Upgrading') {
percent = 50;
} else if (state === 'Succeeded' || state === 'succeeded') {
percent = 100;
}
message.push({
name: key,
state,
percent,
message: _message
});
}
for (const node of this.nodes) {
const hasNode = message.find( O => O.name === node.id);
if (!hasNode) {
message.push({
name: node.id,
state: 'Pending',
percent: 0,
});
}
}
return message;
}
get nodeTotalPercent() {
let out = 0;
for (let i = 0; i < this.nodeUpgradeMessage.length; i++) {
out += this.nodeUpgradeMessage[i].percent;
}
out = Math.floor(out / this.nodeUpgradeMessage.length);
const conditions = this?.status?.conditions || [];
const nodeUpgradedCondition = conditions.find( cond => cond.type === 'NodesUpgraded');
if (out === 100 && !nodeUpgradedCondition) {
out = 99;
}
return out;
}
get sysServiceUpgradeMessage() {
let percent = 0;
let state = 'Pending';
const message = [];
const conditions = this?.status?.conditions || [];
for (let i = 0; i < conditions.length; i++) {
const type = conditions[i].type;
if (type === 'SystemServicesUpgraded') {
if (conditions[i].status === 'True') {
percent = 100;
state = 'Succeeded';
} else {
percent = 50;
}
message.push({
name: 'system services',
state,
percent,
message: conditions[i]?.message
});
}
}
if (message.length === 0) {
message.push({
name: 'system services',
state,
percent,
});
}
return message;
}
get totalPercent() {
const nodePercent = this.nodeTotalPercent * this.nodeUpgradeMessage.length;
const servicePercent = this.sysServiceUpgradeMessage?.[0].percent;
return Math.floor((nodePercent + servicePercent) / (this.nodeUpgradeMessage.length + 1));
}
}
@@ -0,0 +1,54 @@
import { get } from '@shell/utils/object';
import { HCI } from '../types';
import Parse from 'url-parse';
import { findBy } from '@shell/utils/array';
import HarvesterResource from './harvester';
export default class HciUpgradeLog extends HarvesterResource {
get canStartedDownload() {
const conditions = get(this, 'status.conditions');
const status = (findBy(conditions, 'type', 'DownloadReady') || {}).status ;
return status === 'True';
}
downloadLog(filename) {
const parse = Parse(window.history.href);
const clusterId = this.$rootGetters['clusterId'];
const prefix = `/k8s/clusters/${ clusterId }`;
if (this.$rootGetters['isMultiCluster']) {
window.location.href = `${ parse.origin }${ prefix }/v1/harvester/${ HCI.UPGRADE_LOG }s/${ this.id }/download?archiveName=${ filename }`;
} else {
window.location.href = `${ parse.origin }/v1/harvester/${ HCI.UPGRADE_LOG }s/${ this.id }/download?archiveName=${ filename }`;
}
}
fileIsReady(filename) {
const fileArchive = (this.status?.archives || {})[filename];
return fileArchive?.ready === true || fileArchive?.reason;
}
downloadArchivesStatus(filename) {
return (this.status?.archives || {})[filename]?.reason;
}
get latestArchivesFileName() {
const archives = this.status?.archives || {};
const fileNamePrefix = `${ this.metadata.name }-archive-`;
const fileNames = Object.keys(archives).map((filename) => {
return filename.replace(fileNamePrefix, '');
});
const latestFileName = fileNames.sort((a, b) => {
const _a = a.replace(/(\d{2})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z');
const _b = b.replace(/(\d{2})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z');
return Date.parse(_b) - Date.parse(_a);
}).map((filename) => {
return `${ fileNamePrefix }${ filename }`;
});
return latestFileName[0];
}
}
@@ -0,0 +1,161 @@
import { get, clone } from '@shell/utils/object';
import { findBy } from '@shell/utils/array';
import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
import { _CREATE } from '@shell/config/query-params';
import { HCI } from '../types';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
import { BACKUP_TYPE } from '../config/types';
import HarvesterResource from './harvester';
export default class HciVmBackup extends HarvesterResource {
detailPageHeaderActionOverride(realMode) {
const route = this.currentRoute();
if (realMode === _CREATE) {
return route.params.resource === HCI.BACKUP ? this.t('harvester.backup.title') : this.t('harvester.vmSnapshot.title');
}
}
get detailLocation() {
const detailLocation = clone(this._detailLocation);
const route = this.currentRoute();
detailLocation.params.resource = route.params.resource;
return detailLocation;
}
get doneOverride() {
const route = this.currentRoute();
const detailLocation = clone(this._detailLocation);
delete detailLocation.params.namespace;
delete detailLocation.params.id;
detailLocation.params.resource = route.params.resource;
detailLocation.name = `${ HARVESTER_PRODUCT }-c-cluster-resource`;
return detailLocation;
}
get parentNameOverride() {
const route = this.currentRoute();
return this.$rootGetters['i18n/t'](`typeLabel."${ route.params.resource }"`, { count: 1 })?.trim();
}
get parentLocationOverride() {
return this.doneOverride;
}
get _availableActions() {
const toFilter = ['goToEdit', 'cloneYaml', 'goToClone', 'download'];
const out = super._availableActions.filter((action) => {
if (!toFilter.includes(action.action)) {
return action;
}
});
const schema = this.$getters['schemaFor'](HCI.VM);
let canCreateVM = true;
if ( schema && !schema?.collectionMethods.find(x => ['post'].includes(x.toLowerCase())) ) {
canCreateVM = false;
}
return [
{
action: 'restoreExistingVM',
enabled: canCreateVM && this.attachVmExisting && this?.status?.readyToUse,
icon: 'icons icon-refresh',
label: this.t('harvester.action.restoreExistingVM'),
},
{
action: 'restoreNewVM',
enabled: canCreateVM && this?.status?.readyToUse,
icon: 'icons icon-backup',
label: this.t('harvester.action.restoreNewVM'),
},
...out
];
}
restoreExistingVM(resource = this) {
const route = this.currentRoute();
const router = this.currentRouter();
router.push({
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
params: { resource: route.params.resource },
query: { restoreMode: 'existing', resourceName: resource.name }
});
}
restoreNewVM(resource = this) {
const route = this.currentRoute();
const router = this.currentRouter();
router.push({
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
params: { resource: route.params.resource },
query: { restoreMode: 'new', resourceName: resource.name }
});
}
get state() {
let out = 'Pending';
const conditions = get(this, 'status.conditions');
const isProgress = findBy(conditions, 'type', 'InProgress')?.status === 'True';
const isReady = findBy(conditions, 'type', 'Ready')?.status === 'True';
if (this?.status?.readyToUse) {
out = 'Ready';
} else if (isProgress) {
out = 'Progressing';
} else if (!isReady) {
out = 'error';
}
return out;
}
get stateColor() {
const state = this.state;
return colorForState(state);
}
get attachVM() {
return this.spec.source.name;
}
get backupContentName() {
return this?.status?.virtualMachineBackupContentName || '';
}
get backupProgress() {
return {
type: BACKUP_TYPE.BACKUP,
percentage: this.status?.progress === undefined && !this.status?.readyToUse ? 0 : this.status?.progress,
details: { volumes: this.status?.volumeBackups }
};
}
get backupTarget() {
return this?.status?.backupTarget?.endpoint || '';
}
get attachVmExisting() {
const vmList = this.$rootGetters['harvester/all'](HCI.VM);
return !!vmList.find( V => V.metadata.name === this.attachVM);
}
remove() {
const opt = { ...arguments };
opt.params = { propagationPolicy: 'Foreground' };
return this._remove(opt);
}
}
@@ -0,0 +1,296 @@
import Vue from 'vue';
import { HCI } from '../types';
import {
DESCRIPTION,
ANNOTATIONS_TO_IGNORE_REGEX,
} from '@shell/config/labels-annotations';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { get, clone } from '@shell/utils/object';
import { formatSi } from '@shell/utils/units';
import { ucFirst } from '@shell/utils/string';
import { stateDisplay, colorForState } from '@shell/plugins/dashboard-store/resource-class';
import { _CLONE } from '@shell/config/query-params';
import HarvesterResource from './harvester';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
function isReady() {
function getStatusConditionOfType(type, defaultValue = []) {
const conditions = Array.isArray(get(this, 'status.conditions')) ? this.status.conditions : defaultValue;
return conditions.find( cond => cond.type === type);
}
const initialized = getStatusConditionOfType.call(this, 'Initialized');
const imported = getStatusConditionOfType.call(this, 'Imported');
const isCompleted = this.status?.progress === 100;
if ([initialized?.status, imported?.status].includes('False')) {
return false;
} else {
return isCompleted && true;
}
}
export default class HciVmImage extends HarvesterResource {
get availableActions() {
let out = super._availableActions;
const toFilter = ['goToEditYaml'];
out = out.filter( A => !toFilter.includes(A.action));
const schema = this.$getters['schemaFor'](HCI.VM);
let canCreateVM = true;
if ( schema && !schema?.collectionMethods.find(x => ['post'].includes(x.toLowerCase())) ) {
canCreateVM = false;
}
return [
{
action: 'createFromImage',
enabled: canCreateVM,
icon: 'icon icon-fw icon-spinner',
label: this.t('harvester.action.createVM'),
disabled: !this.isReady,
},
{
action: 'download',
enabled: this.links?.download,
icon: 'icon icon-download',
label: this.t('asyncButton.download.action'),
},
...out
];
}
applyDefaults(resources = this, realMode) {
if (realMode !== _CLONE) {
Vue.set(this.metadata, 'labels', { [HCI_ANNOTATIONS.OS_TYPE]: '', [HCI_ANNOTATIONS.IMAGE_SUFFIX]: '' });
Vue.set(this.metadata, 'annotations', { [HCI_ANNOTATIONS.STORAGE_CLASS]: '' });
}
}
createFromImage() {
const router = this.currentRouter();
router.push({
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
params: { resource: HCI.VM },
query: { image: this.id }
});
}
cleanForNew() {
this.$dispatch(`cleanForNew`, this);
delete this.spec.displayName;
}
get nameDisplay() {
return this.spec?.displayName;
}
get isOSImage() {
return this?.metadata?.annotations?.[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] === 'True';
}
get isReady() {
return isReady.call(this);
}
get stateDisplay() {
const initialized = this.getStatusConditionOfType('Initialized');
const imported = this.getStatusConditionOfType('Imported');
if (imported?.status === 'Unknown') {
if (this.spec.sourceType === 'download') {
return 'Downloading';
}
if (this.spec.sourceType === 'upload') {
if (this.uploadError) {
return 'Failed';
}
return 'Uploading';
}
return 'Exporting';
}
if (initialized?.message || imported?.message) {
return 'Failed';
}
return stateDisplay(this.metadata.state.name);
}
get imageMessage() {
if (this.uploadError) {
return ucFirst(this.uploadError);
}
const conditions = this?.status?.conditions || [];
const initialized = conditions.find( cond => cond.type === 'Initialized');
const imported = conditions.find( cond => cond.type === 'Imported');
const message = initialized?.message || imported?.message;
return ucFirst(message);
}
get stateBackground() {
return colorForState(this.stateDisplay).replace('text-', 'bg-');
}
get imageSource() {
return get(this, `spec.sourceType`) || 'download';
}
get progress() {
return this?.status?.progress || 0;
}
get annotationsToIgnoreRegexes() {
return [DESCRIPTION].concat(ANNOTATIONS_TO_IGNORE_REGEX);
}
get downSize() {
const size = this.status?.size;
if (!size) {
return '-';
}
return formatSi(size, {
increment: 1024,
maxPrecision: 2,
suffix: 'B',
firstSuffix: 'B',
});
}
getStatusConditionOfType(type, defaultValue = []) {
const conditions = Array.isArray(get(this, 'status.conditions')) ? this.status.conditions : defaultValue;
return conditions.find( cond => cond.type === type);
}
get stateObj() {
const state = clone(this.metadata?.state);
const initialized = this.getStatusConditionOfType('Initialized');
const imported = this.getStatusConditionOfType('Imported');
if ([initialized?.status, imported?.status].includes('False') || this.uploadError) {
state.error = true;
}
return state;
}
get stateDescription() {
return this.imageMessage;
}
get displayName() {
return this.spec?.displayName;
}
get storageClassName() {
return this.status?.storageClassName || '';
}
get uploadImage() {
return async(file) => {
const formData = new FormData();
formData.append('chunk', file);
try {
this.$ctx.commit('harvester-common/uploadStart', this.metadata.name, { root: true });
await this.doAction('upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
'File-Size': file.size,
},
params: { size: file.size },
});
} catch (err) {
this.$ctx.commit('harvester-common/uploadError', { name: this.name, message: err.message }, { root: true });
this.$ctx.commit('harvester-common/uploadEnd', this.metadata.name, { root: true });
return Promise.reject(err);
}
this.$ctx.commit('harvester-common/uploadEnd', this.metadata.name, { root: true });
};
}
get uploadError() {
return this.$rootGetters['harvester-common/uploadingImageError'](this.name);
}
get imageSuffix() {
return this.metadata?.labels?.[HCI_ANNOTATIONS.IMAGE_SUFFIX];
}
get imageOSType() {
return this.metadata?.labels?.[HCI_ANNOTATIONS.OS_TYPE];
}
get customValidationRules() {
const out = [];
if (this.imageSource === 'download') {
const urlFormat = {
nullable: false,
path: 'spec.url',
validators: ['imageUrl'],
};
const urlRequired = {
nullable: false,
path: 'spec.url',
required: true,
translationKey: 'harvester.image.url'
};
out.push(urlFormat, urlRequired);
}
if (this.imageSource === 'upload') {
const fileRequired = {
nullable: false,
path: 'metadata.annotations',
validators: ['fileRequired'],
};
out.push(fileRequired);
}
if (this.spec?.checksum?.length) {
const checksumFormat = {
path: 'spec.checksum',
validators: ['hashSHA512'],
};
out.push(checksumFormat);
}
return [
{
nullable: false,
path: 'spec.displayName',
required: true,
minLength: 1,
maxLength: 63,
translationKey: 'generic.name',
},
...out
];
}
download() {
window.location.href = this.links.download;
}
}
@@ -0,0 +1,43 @@
import HarvesterResource from './harvester';
export default class HciVmRestore extends HarvesterResource {
get isComplete() {
return this?.status?.complete || false;
}
get pvcNames() {
const restores = this?.status?.restores || [];
const out = restores.map( O => O?.persistentVolumeClaimSpec?.name);
return out;
}
get customValidationRules() {
const isNew = this.spec?.newVM;
const rules = [{
nullable: false,
path: 'spec.target.name',
required: true,
minLength: 1,
maxLength: 63,
translationKey: 'harvester.fields.virtualName',
}, {
nullable: false,
path: 'spec.virtualMachineBackupName',
required: true,
translationKey: 'harvester.backup.restore.backup',
}];
if (!isNew) {
rules.push({
path: 'spec.deletionPolicy',
required: true,
type: 'string',
translationKey: 'harvester.backup.restore.deletePreviousVolumes',
});
}
return rules;
}
}
@@ -0,0 +1,70 @@
import { HCI } from '../types';
import { MODE, _CREATE } from '@shell/config/query-params';
import HarvesterResource from './harvester';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
export default class HciVmTemplate extends HarvesterResource {
get availableActions() {
const toFilter = ['goToEdit', 'cloneYaml', 'goToClone', 'goToEditYaml', 'download'];
const out = super._availableActions.filter((action) => {
if (action.altAction === 'remove') {
action.bulkable = false;
}
if (!toFilter.includes(action.action)) {
return action;
}
});
const schema = this.$getters['schemaFor'](HCI.VM);
let canCreateVM = true;
if ( schema && !schema?.collectionMethods.find(x => ['post'].includes(x.toLowerCase())) ) {
canCreateVM = false;
}
return [
{
action: 'createFromTemplate',
enabled: canCreateVM,
icon: 'icon icon-spinner',
label: this.t('harvester.action.createVM'),
},
{
action: 'addVersion',
enabled: this.canCreate,
icon: 'icon icon-fw icon-circle-plus',
label: this.t('harvester.action.addTemplateVersion'),
},
...out
];
}
createFromTemplate() {
const router = this.currentRouter();
router.push({
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
params: { resource: HCI.VM },
query: { templateId: this.id, versionId: this.spec.defaultVersionId }
});
}
addVersion(moreQuery = {}) {
const router = this.currentRouter();
router.push({
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
params: { resource: HCI.VM_VERSION },
query: {
[MODE]: _CREATE,
templateId: this.id
}
});
}
get defaultVersion() {
return this.status?.defaultVersion;
}
}
@@ -0,0 +1,277 @@
import Vue from 'vue';
import { find, pickBy, omitBy } from 'lodash';
import { HCI } from '../types';
import {
AS, MODE, _VIEW, _CONFIG, _UNFLAG, _EDIT
} from '@shell/config/query-params';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import HarvesterResource from './harvester';
import { findBy } from '@shell/utils/array';
import { get, set } from '@shell/utils/object';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
import { LABELS_TO_IGNORE_REGEX } from '@shell/config/labels-annotations';
import { matchesSomeRegex } from '@shell/utils/string';
export default class HciVmTemplateVersion extends HarvesterResource {
get availableActions() {
let out = super._availableActions;
const toFilter = ['goToClone', 'cloneYaml', 'goToViewConfig', 'goToEditYaml', 'goToViewYaml'];
out = out.filter((action) => {
if (!toFilter.includes(action.action)) {
return action;
}
});
const schema = this.$getters['schemaFor'](HCI.VM);
let canCreateVM = true;
if ( schema && !schema?.collectionMethods.find(x => ['post'].includes(x.toLowerCase())) ) {
canCreateVM = false;
}
return [
{
action: 'launchFromTemplate',
icon: 'icon icon-spinner',
disabled: !canCreateVM || !this.isReady,
label: this.t('harvester.action.launchFormTemplate'),
},
{
action: 'cloneTemplate',
enabled: this.currentTemplate?.canCreate,
icon: 'icon icon-fw icon-edit',
label: this.t('harvester.action.modifyTemplate'),
},
{
action: 'setDefaultVersion',
enabled: this.currentTemplate?.canCreate,
icon: 'icon icon-fw icon-checkmark',
label: this.t('harvester.action.setDefaultVersion'),
},
{
action: 'goToViewConfig',
label: this.t('action.view'),
icon: 'icon icon-edit',
},
...out
];
}
applyDefaults() {
const spec = {
vm: {
metadata: { annotations: { [HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE]: '[]' } },
spec: {
runStrategy: 'RerunOnFailure',
template: {
metadata: { annotations: {} },
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: {},
}
}
}
}
};
Vue.set(this, 'spec', spec);
}
get canDelete() {
return this.hasLink('remove') && this.$rootGetters['type-map/optionsFor'](this.type).isRemovable && !this.isDefaultVersion;
}
get template() {
return this.$rootGetters['harvester/all'](HCI.VM_TEMPLATE).find((T) => {
return T.id === this.spec.templateId;
});
}
get isReady() {
const conditions = get(this, 'status.conditions');
const readyCondition = findBy(conditions, 'type', 'ready');
// Compatibility processing
return readyCondition ? readyCondition?.status === 'True' : true;
}
get stateDisplay() {
if (this.isReady) {
return 'Active';
} else {
return 'Not Ready';
}
}
get stateColor() {
const state = this.stateDisplay;
return colorForState(state);
}
get version() {
return this?.status?.version;
}
get templates() {
return this.$rootGetters['harvester/all'](HCI.VM_TEMPLATE);
}
get machineType() {
return this.vm?.spec?.template?.spec?.domain?.machine?.type || '';
}
get templateId() {
return this.spec.templateId;
}
launchFromTemplate() {
const templateResource = this.currentTemplate;
const templateId = templateResource.id;
const launchVersion = this.id;
const router = this.currentRouter();
router.push({
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
params: { resource: HCI.VM },
query: { templateId, versionId: launchVersion }
});
}
cloneTemplate(moreQuery = {}) {
const location = this.detailLocation;
location.query = {
...location.query,
[MODE]: _EDIT,
[AS]: _UNFLAG,
...moreQuery
};
this.currentRouter().push(location);
}
goToViewConfig(moreQuery = {}) {
const location = this.detailLocation;
location.query = {
...location.query,
[MODE]: _VIEW,
[AS]: _CONFIG,
templateId: this.templateId,
...moreQuery
};
this.currentRouter().push(location);
}
get currentTemplate() {
return find(this.templates, T => T.id === this.templateId);
}
async setDefaultVersion(moreQuery = {}) {
const templateResource = this.currentTemplate;
templateResource.spec.defaultVersionId = this.id;
await templateResource.save();
}
get defaultVersion() {
const templates = this.$rootGetters['harvester/all'](HCI.VM_TEMPLATE);
const template = templates.find(T => this.templateId === T.id);
return template?.status?.defaultVersion;
}
get isDefaultVersion() {
return this.defaultVersion === this?.status?.version;
}
get customValidationRules() {
const rules = [
// {
// nullable: false,
// path: 'spec.vm.spec.template.spec.domain.cpu.cores',
// min: 1,
// max: 100,
// required: true,
// translationKey: 'harvester.fields.cpu',
// },
// {
// nullable: false,
// path: 'spec.vm.spec.template.spec.domain.resources.requests.memory',
// required: false,
// translationKey: 'harvester.fields.memory',
// },
// {
// nullable: false,
// path: 'spec.vm.spec.template.spec',
// validators: ['vmNetworks'],
// },
// {
// nullable: false,
// path: 'spec.vm.spec',
// validators: ['vmDisks:isVMTemplate'],
// },
];
return rules;
}
get instanceLabels() {
const all = this.spec?.vm?.spec?.template?.metadata?.labels || {};
return omitBy(all, (value, key) => {
return matchesSomeRegex(key, LABELS_TO_IGNORE_REGEX);
});
}
setInstanceLabels(val) {
if ( !this.spec?.vm?.spec?.template?.metadata?.labels ) {
set(this, 'spec.vm.spec.template.metadata.labels', {});
}
const all = this.spec.vm.spec.template.metadata.labels || {};
const wasIgnored = pickBy(all, (value, key) => {
return matchesSomeRegex(key, LABELS_TO_IGNORE_REGEX);
});
Vue.set(this.spec.vm.spec.template.metadata, 'labels', { ...wasIgnored, ...val });
}
}
@@ -0,0 +1,89 @@
import Vue from 'vue';
import SteveModel from '@shell/plugins/steve/steve-class';
import { HCI } from '@shell/config/labels-annotations';
export default class NetworkAttachmentDef extends SteveModel {
applyDefaults() {
const spec = this.spec || {
config: JSON.stringify({
cniVersion: '0.3.1',
name: '',
type: 'bridge',
bridge: '',
promiscMode: true,
vlan: '',
ipam: {}
})
};
Vue.set(this, 'spec', spec);
}
get parseConfig() {
try {
return JSON.parse(this.spec.config) || {};
} catch (err) {
return {};
}
}
get isIpamStatic() {
return this.parseConfig.ipam?.type === 'static';
}
get clusterNetwork() {
return this?.metadata?.labels?.[HCI.CLUSTER_NETWORK];
}
get vlanType() {
const labels = this.metadata?.labels || {};
const type = labels[HCI.NETWORK_TYPE];
return type;
}
get vlanId() {
return this.vlanType === 'UntaggedNetwork' ? 'N/A' : this.parseConfig.vlan;
}
get customValidationRules() {
const rules = [
{
nullable: false,
path: 'metadata.name',
required: true,
minLength: 1,
maxLength: 63,
translationKey: 'harvester.fields.name'
}
];
return rules;
}
get connectivity() {
const annotations = this.metadata?.annotations || {};
const route = annotations[HCI.NETWORK_ROUTE];
let config = {};
if (this.vlanType === 'UntaggedNetwork') {
return 'N/A';
}
try {
config = JSON.parse(route || '{}');
} catch {
return 'invalid';
}
const connectivity = config.connectivity;
if (connectivity === 'false') {
return 'inactive';
} else if (connectivity === 'true') {
return 'active';
} else {
return connectivity;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,143 @@
import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
import { NODE } from '@shell/config/types';
import { HCI } from '../types';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import HarvesterResource from './harvester';
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 VMIPhase = {
Pending: 'Pending',
Scheduling: 'Scheduling',
Scheduled: 'Scheduled',
Running: 'Running',
Succeeded: 'Succeeded',
Failed: 'Failed',
Unknown: 'Unknown',
};
export default class VirtVmInstance extends HarvesterResource {
get _availableActions() {
const out = super._availableActions;
const actions = out.find((O) => {
return O.action === 'promptRemove';
});
return [actions];
}
get stateDisplay() {
if (this?.metadata?.deletionTimestamp) {
return 'Terminating';
}
return this?.status?.phase;
}
get stateBackground() {
return colorForState(this.stateDisplay).replace('text-', 'bg-');
}
get stateColor() {
const state = this.stateDisplay;
return colorForState(state);
}
get vmimResource() {
const all = this.$rootGetters['harvester/all'](HCI.VMIM) || [];
const vmimList = all.filter(vmim => vmim.spec?.vmiName === this.metadata?.name);
if (vmimList.length === 0) {
return [];
}
vmimList.sort((a, b) => {
return a?.metadata?.creationTimestamp > b?.metadata?.creationTimestamp ? -1 : 1;
});
return vmimList[0];
}
get migrationState() {
const state = this.metadata?.annotations?.[HCI_ANNOTATIONS.MIGRATION_STATE];
if (this.vmimResource?.status?.phase === VMIPhase.Failed) {
return {
type: 'migration',
status: VMIPhase.Failed
};
}
if (this.vmimResource?.status?.phase && state) {
return {
type: 'migration',
status: state
};
}
return null;
}
get migrationStateBackground() {
const state = this.migrationState.status;
return colorForState(state).replace('text-', 'bg-');
}
get isPaused() {
const conditions = this?.status?.conditions || [];
const isPause = conditions.filter(cond => cond.type === PAUSED).length > 0;
return isPause ? {
status: PAUSED,
message: PAUSED_VM_MODAL_MESSAGE
} : null;
}
get isRunning() {
if (this?.status?.phase === VMIPhase.Running) {
return { status: VMIPhase.Running };
}
return null;
}
get isTerminated() {
const conditions = this?.status?.conditions || [];
return conditions.find(cond => cond.type === 'Ready')?.status === 'False';
}
get getVMIApiPath() {
const clusterId = this.$rootGetters['clusterId'];
if (this.$rootGetters['isMultiCluster']) {
const prefix = `/k8s/clusters/${ clusterId }`;
return `${ prefix }/apis/subresources.kubevirt.io/v1/namespaces/${ this.metadata.namespace }/virtualmachineinstances/${ this.name }/vnc`;
} else {
return `/apis/subresources.kubevirt.io/v1/namespaces/${ this.metadata.namespace }/virtualmachineinstances/${ this.name }/vnc`;
}
}
get realAttachNodeName() {
const nodeName = this?.status?.nodeName;
const node = this.$getters['byId'](NODE, nodeName);
return node?.nameDisplay || '';
}
get getSerialConsolePath() {
const clusterId = this.$rootGetters['clusterId'];
if (this.$rootGetters['isMultiCluster']) {
const prefix = `/k8s/clusters/${ clusterId }`;
return `${ prefix }/apis/subresources.kubevirt.io/v1/namespaces/${ this.metadata.namespace }/virtualmachineinstances/${ this.name }/console`;
} else {
return `/apis/subresources.kubevirt.io/v1/namespaces/${ this.metadata.namespace }/virtualmachineinstances/${ this.name }/console`;
}
}
}
@@ -0,0 +1,105 @@
import Vue from 'vue';
import { clone } from '@shell/utils/object';
import HarvesterResource from '@pkg/harvester/models/harvester';
import { HCI } from '@pkg/harvester/types';
const NOT_READY = 'Not Ready';
export default class HciLB extends HarvesterResource {
applyDefaults() {
const spec = this.spec || {};
const meta = this.metadata || {};
spec.ranges = spec.ranges || [];
spec.selector = spec.selector || {};
spec.selector.network = spec.selector.network || '';
Vue.set(this, 'spec', spec);
Vue.set(this, 'metadata', meta);
}
get customValidationRules() {
return [
{
nullable: false,
path: 'metadata.name',
required: true,
translationKey: 'generic.name',
type: 'dnsLabel',
},
{
nullable: false,
path: 'spec.ranges',
type: 'array',
validators: ['ranges'],
}
];
}
get subnetDisplay() {
const ranges = this.spec?.ranges || [];
return ranges.map(r => r.subnet).join(', ');
}
get startIPDisplay() {
const ranges = this.spec?.ranges || [];
return ranges.filter(r => r.startIP).map(r => r.startIP).join(', ');
}
get endIPDisplay() {
const ranges = this.spec?.ranges || [];
return ranges.filter(r => r.endIP).map(r => r.endIP).join(', ');
}
get details() {
const out = [{
label: this.t('harvester.ipPool.availableIP.label'),
content: this.status?.available,
}];
return out;
}
get doneOverride() {
const detailLocation = clone(this.listLocation);
detailLocation.params.resource = HCI.IP_POOL;
return detailLocation;
}
get parentLocationOverride() {
return {
...this.listLocation,
params: {
...this.listLocation.params,
resource: HCI.IP_POOL,
}
};
}
get isReady() {
const readyCondition = (this?.status?.conditions || []).find(c => c.type === 'Ready') || {};
return readyCondition?.status === 'True';
}
get stateDisplay() {
if (!this.isReady) {
return NOT_READY;
}
return super.stateDisplay;
}
get stateBackground() {
if (!this.isReady) {
return 'bg-warning';
}
return super.stateBackground;
}
}
@@ -0,0 +1,112 @@
import { set, clone } from '@shell/utils/object';
import HarvesterResource from '@pkg/harvester/models/harvester';
import { HCI } from '@pkg/harvester/types';
const WORKLOAD_TYPE_VM = 'vm';
const NOT_READY = 'Not Ready';
export default class HciLB extends HarvesterResource {
applyDefaults() {
const spec = this.spec || {};
const meta = this.metadata || {};
spec.workloadType = spec.workloadType || WORKLOAD_TYPE_VM;
spec.ipam = spec.ipam || 'dhcp';
spec.healthCheck = spec.healthCheck || {};
spec.backendServerSelector = spec.backendServerSelector || {};
set(this, 'spec', spec);
set(this, 'metadata', meta);
}
get workloadTypeDisplay() {
const workloadType = this.spec?.workloadType || 'cluster';
return this.$rootGetters['i18n/t'](`harvester.loadBalancer.workloadType.options.${ workloadType }`);
}
get ipamDisplay() {
const ipam = this.spec?.ipam;
return this.$rootGetters['i18n/t'](`harvester.loadBalancer.ipam.options.${ ipam }`);
}
get customValidationRules() {
return [
{
nullable: false,
path: 'metadata.name',
required: true,
translationKey: 'generic.name',
type: 'dnsLabel',
},
];
}
get canCustomEdit() {
return this.$rootGetters['type-map/hasCustomEdit'](this.type, this.id) && this.spec.workloadType === WORKLOAD_TYPE_VM;
}
get canUpdate() {
return this.hasLink('update') && this.$rootGetters['type-map/optionsFor'](this.type).isEditable && this.spec.workloadType === WORKLOAD_TYPE_VM;
}
get canClone() {
return this.spec.workloadType === WORKLOAD_TYPE_VM;
}
get details() {
const out = [{
label: this.t('harvester.loadBalancer.ipam.label'),
content: this.ipamDisplay,
}, {
label: this.t('harvester.loadBalancer.workloadType.label'),
content: this.workloadTypeDisplay,
}, {
label: this.t('tableHeaders.address'),
content: this.status.address,
}];
return out;
}
get doneOverride() {
const detailLocation = clone(this.listLocation);
detailLocation.params.resource = HCI.LB;
return detailLocation;
}
get parentLocationOverride() {
return {
...this.listLocation,
params: {
...this.listLocation.params,
resource: HCI.LB,
}
};
}
get isReady() {
const readyCondition = (this?.status?.conditions || []).find(c => c.type === 'Ready') || {};
return readyCondition?.status === 'True';
}
get stateDisplay() {
if (!this.isReady) {
return NOT_READY;
}
return super.stateDisplay;
}
get stateBackground() {
if (!this.isReady) {
return 'bg-warning';
}
return super.stateBackground;
}
}
+44
View File
@@ -0,0 +1,44 @@
import SteveModel from '@shell/plugins/steve/steve-class';
const HEALTHY = 'healthy';
const WARNING = 'warning';
export default class LonghornNode extends SteveModel {
get used() {
let out = 0;
this.disks.filter(d => d.allowScheduling).map((disk) => {
if (disk?.storageAvailable && disk?.storageMaximum) {
out += disk.storageMaximum - disk.storageAvailable;
}
});
return out;
}
get disks() {
const diskStatus = this?.status?.diskStatus || {};
const diskSpec = this?.spec?.disks || {};
return Object.keys(diskSpec).map((key) => {
const conditions = diskStatus[key]?.conditions || [];
const readyCondition = conditions.find(c => c.type === 'Ready') || {};
const schedulableCondition = conditions.find(c => c.type === 'Schedulable') || {};
let state;
if (readyCondition?.status !== 'True' || schedulableCondition?.status !== 'True') {
state = WARNING;
} else {
state = HEALTHY;
}
return {
...diskStatus[key],
...diskSpec[key],
id: key,
state,
};
}) || [];
}
}
@@ -0,0 +1,47 @@
import HarvesterResource from './harvester';
import { set } from '@shell/utils/object';
export default class HciInventory extends HarvesterResource {
applyDefaults() {
const defaultSpec = {
baseboardSpec: {
connection: {
authSecretRef: {},
insecureTLS: false,
}
},
events: {
enabled: true,
pollingInterval: '1h',
},
primaryDisk: '',
managementInterfaceMacAddress: '',
};
set(this, 'metadata.annotations', {});
set(this, 'spec', this.spec || defaultSpec);
set(this, 'spec.baseboardSpec', this.spec?.baseboardSpec || defaultSpec.baseboardSpec);
set(this, 'spec.baseboardSpec.connection', this.spec?.baseboardSpec?.connection || defaultSpec.baseboardSpec.connection);
set(this, 'spec.baseboardSpec.connection.authSecretRef', this.spec?.baseboardSpec?.connection?.authSecretRef || {});
}
get warningMessages() {
const out = [];
if (this.metadata?.state?.error) {
out.push({ text: this.metadata?.state.message });
}
return out;
}
get customValidationRules() {
return [
{
path: 'spec.events.pollingInterval',
translationKey: 'harvester.seeder.inventory.pollingInterval.label',
validators: ['interval'],
},
];
}
}
@@ -0,0 +1,69 @@
import { HCI } from '../types';
import { clone } from '@shell/utils/object';
import HarvesterResource from './harvester';
import { NODE } from '@shell/config/types';
export default class HciClusterNetwork extends HarvesterResource {
get doneOverride() {
const detailLocation = clone(this.listLocation);
detailLocation.params.resource = HCI.CLUSTER_NETWORK;
return detailLocation;
}
get parentLocationOverride() {
return {
...this.listLocation,
params: {
...this.listLocation.params,
resource: HCI.CLUSTER_NETWORK
}
};
}
get canDelete() {
return this._canDelete && this.id !== 'mgmt';
}
get canUpdate() {
return this.hasLink('update') && this.$rootGetters['type-map/optionsFor'](this.type).isEditable && this.id !== 'mgmt';
}
get isReady() {
const readyCondition = (this?.status?.conditions || []).find(c => c.type === 'ready') || {};
return readyCondition?.status === 'True';
}
get inStore() {
return this.$rootGetters['currentProduct'].inStore;
}
get nodes() {
const nodes = this.$rootGetters[`${ this.inStore }/all`](NODE);
// filter out witness nodes and unschedulable nodes
return nodes.filter(n => !n.isUnSchedulable && n.isEtcd !== 'true');
}
get vlanStatuses() {
const vlanStatuses = this.$rootGetters[`${ this.inStore }/all`](HCI.VLAN_STATUS);
const nodeIds = this.nodes.map(n => n.id);
return vlanStatuses.filter((s) => {
return nodeIds.includes(s?.status?.node) &&
this.id === s?.status?.clusterNetwork;
}) || [];
}
get isReadyForStorageNetwork() {
if (this.id === 'mgmt') {
return true;
} else {
const readyStatuses = this.vlanStatuses.filter(s => s.isReady) || [];
return readyStatuses.length === this.nodes.length && this.isReady;
}
}
}
@@ -0,0 +1,34 @@
import { findBy } from '@shell/utils/array';
import { get } from '@shell/utils/object';
import { HCI } from '../types';
import HarvesterResource from './harvester';
export default class HciNodeNetwork extends HarvesterResource {
get message() {
const conditions = get(this, 'status.conditions');
return (findBy(conditions, 'type', 'Ready') || {}).message ;
}
get isReady() {
const conditions = get(this, 'status.conditions');
return (findBy(conditions, 'type', 'Ready') || {})?.status === 'True';
}
get nics() {
return this?.status?.nics || [];
}
get attachNodeName() {
return get(this, `metadata.labels."network.harvesterhci.io/nodename"`) || '';
}
get linkMessage() {
return {
name: this.attachNodeName,
message: this.message,
to: `${ HCI.HOST }/${ this.attachNodeName }?mode=edit`
};
}
}
@@ -0,0 +1,137 @@
import { isEmpty } from 'lodash';
import { set, clone } from '@shell/utils/object';
import HarvesterResource from './harvester';
import { HCI } from '../types';
import { insertAt } from '@shell/utils/array';
import { HOSTNAME } from '@shell/config/labels-annotations';
import { matching } from '@shell/utils/selector';
import { NODE } from '@shell/config/types';
const NOT_READY = 'Not Ready';
export default class HciVlanConfig extends HarvesterResource {
applyDefaults() {
const defaultSpec = {
uplink: {
nics: [],
linkAttributes: {},
bondOptions: { mode: 'active-backup' },
},
};
set(this, 'spec', this.spec || defaultSpec);
set(this, 'spec.uplink.linkAttributes', this.spec?.uplink?.linkAttributes || {});
set(this, 'spec.uplink.bondOptions', this.spec?.uplink?.bondOptions || {});
}
get groupByClusterNetwork() {
return this.spec?.clusterNetwork;
}
get doneOverride() {
const detailLocation = clone(this.listLocation);
detailLocation.params.resource = HCI.CLUSTER_NETWORK;
return detailLocation;
}
get parentLocationOverride() {
return {
...this.listLocation,
params: {
...this.listLocation.params,
resource: HCI.CLUSTER_NETWORK
}
};
}
get typeDisplay() {
return 'VLAN';
}
get _availableActions() {
const out = super._availableActions;
insertAt(out, 0, this.migrateAction);
return out;
}
get migrateAction() {
return {
action: 'migrate',
icon: 'icon icon-copy',
label: this.t('harvester.vlanConfig.action.migrate'),
};
}
migrate(resources = this) {
this.$dispatch('promptModal', {
resources,
component: 'HarvesterVlanConfigMigrateDialog',
});
}
get inStore() {
return this.$rootGetters['currentProduct'].inStore;
}
get vlanStatuses() {
const nodes = this.nodes.map(n => n.id) || [];
const vlanStatuses = this.$rootGetters[`${ this.inStore }/all`](HCI.VLAN_STATUS);
return vlanStatuses.filter((s) => {
return nodes.includes(s?.status?.node) &&
this.id === s?.status?.vlanConfig;
}) || [];
}
get isReady() {
if (this.nodes.length === 0 || (this.vlanStatuses.length !== this.nodes.length)) {
return false;
} else {
const states = this.vlanStatuses.filter((s) => {
return s.isReady;
});
return states.length === this.vlanStatuses.length;
}
}
get selectedNodes() {
const nodeSelector = this.spec?.nodeSelector || {};
const nodes = this.$rootGetters[`${ this.inStore }/all`](NODE);
if (isEmpty(nodeSelector)) {
return nodes;
} else if (nodeSelector[HOSTNAME] && Object.keys(nodeSelector).length === 1) {
return nodes.filter(n => n.id === nodeSelector[HOSTNAME]) || [];
} else {
const matchNodes = matching(nodes || [], nodeSelector).map(n => n.id);
return nodes.filter(n => matchNodes.includes(n.id));
}
}
get nodes() {
return this.selectedNodes.filter(n => !n.isUnSchedulable && n.isEtcd !== 'true');
}
get stateDisplay() {
if (!this.isReady) {
return NOT_READY;
}
return super.stateDisplay;
}
get stateBackground() {
if (!this.isReady) {
return 'bg-warning';
}
return super.stateBackground;
}
}
@@ -0,0 +1,12 @@
import { findBy } from '@shell/utils/array';
import HarvesterResource from './harvester';
export default class HciVlanStatus extends HarvesterResource {
get isReady() {
const conditions = this.status?.conditions || [];
const readyCondition = findBy(conditions, 'type', 'ready') || {};
return readyCondition.status === 'True';
}
}