mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-01 03:36:04 +08:00
Add pkg/harvester components + shell portings - 1
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
@@ -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) : [];
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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']
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user