fix conflict

This commit is contained in:
Nancy Butler 2022-07-11 15:49:19 -07:00
parent 840d1518f1
commit 38dfc71654
28 changed files with 805 additions and 1334 deletions

View File

@ -1,5 +1,6 @@
<script> <script>
import compact from 'lodash/compact'; import compact from 'lodash/compact';
import { OFF } from '../../models/harvester/kubevirt.io.virtualmachine';
import { get } from '@shell/utils/object'; import { get } from '@shell/utils/object';
import { isIpv4 } from '@shell/utils/string'; import { isIpv4 } from '@shell/utils/string';
import { HCI as HCI_ANNOTATIONS } from '@shell/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@shell/config/labels-annotations';
@ -13,12 +14,16 @@ export default {
components: { CopyToClipboard }, components: { CopyToClipboard },
props: { props: {
value: { value: {
type: String, type: String,
default: '' default: ''
}, },
row: { row: {
type: Object, type: Object,
required: true required: true
},
col: {
type: Object,
default: () => {}
} }
}, },
@ -31,16 +36,11 @@ export default {
}, },
networkAnnotationIP() { networkAnnotationIP() {
if (this.row.actualState !== 'Running') { if (this.row.actualState !== 'Running') { // TODO: Running
// TODO: Running
return []; return [];
} }
const annotationIp = const annotationIp = get(this.row, `metadata.annotations."${ HCI_ANNOTATIONS.NETWORK_IPS }"`) || '[]';
get(
this.row,
`metadata.annotations."${ HCI_ANNOTATIONS.NETWORK_IPS }"`
) || '[]';
// Obtain IP from VM annotation, remove the CIDR suffix number if CIDR Exist // Obtain IP from VM annotation, remove the CIDR suffix number if CIDR Exist
try { try {
@ -82,8 +82,8 @@ export default {
showIP() { showIP() {
return this.row.stateDisplay !== OFF; return this.row.stateDisplay !== OFF;
} },
} },
}; };
</script> </script>

View File

@ -4,9 +4,9 @@ import { Banner } from '@components/Banner';
import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units'; import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
import { HCI as HCI_ANNOTATIONS } from '@shell/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@shell/config/labels-annotations';
import { LONGHORN, METRIC, HCI } from '@shell/config/types'; import { LONGHORN, METRIC, HCI } from '@shell/config/types';
import HarvesterCPUUsed from '@shell/components/formatter/HarvesterCPUUsed'; import HarvesterCPUUsed from '../../components/formatter/HarvesterCPUUsed';
import HarvesterMemoryUsed from '@shell/components/formatter/HarvesterMemoryUsed'; import HarvesterMemoryUsed from '../../components/formatter/HarvesterMemoryUsed';
import HarvesterStorageUsed from '@shell/components/formatter/HarvesterStorageUsed'; import HarvesterStorageUsed from '../../components/formatter/HarvesterStorageUsed';
const COMPLETE = 'complete'; const COMPLETE = 'complete';
const NONE = 'none'; const NONE = 'none';
@ -21,13 +21,13 @@ export default {
Banner, Banner,
HarvesterCPUUsed, HarvesterCPUUsed,
HarvesterMemoryUsed, HarvesterMemoryUsed,
HarvesterStorageUsed HarvesterStorageUsed,
}, },
props: { props: {
value: { value: {
type: Object, type: Object,
required: true required: true,
}, },
metrics: { metrics: {
@ -55,25 +55,18 @@ export default {
computed: { computed: {
customName() { customName() {
return this.value.metadata?.annotations?.[ return this.value.metadata?.annotations?.[HCI_ANNOTATIONS.HOST_CUSTOM_NAME];
HCI_ANNOTATIONS.HOST_CUSTOM_NAME
];
}, },
consoleUrl() { consoleUrl() {
const consoleUrl = this.value.metadata?.annotations?.[ const consoleUrl = this.value.metadata?.annotations?.[HCI_ANNOTATIONS.HOST_CONSOLE_URL];
HCI_ANNOTATIONS.HOST_CONSOLE_URL
];
let value = consoleUrl; let value = consoleUrl;
if (!consoleUrl) { if (!consoleUrl) {
return ''; return '';
} }
if ( if (!consoleUrl.startsWith('http://') && !consoleUrl.startsWith('https://')) {
!consoleUrl.startsWith('http://') &&
!consoleUrl.startsWith('https://')
) {
value = `http://${ consoleUrl }`; value = `http://${ consoleUrl }`;
} }
@ -125,10 +118,7 @@ export default {
storageUsage() { storageUsage() {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
const longhornNode = this.$store.getters[`${ inStore }/byId`]( const longhornNode = this.$store.getters[`${ inStore }/byId`](LONGHORN.NODES, `longhorn-system/${ this.value.id }`);
LONGHORN.NODES,
`longhorn-system/${ this.value.id }`
);
let out = 0; let out = 0;
const diskStatus = longhornNode?.status?.diskStatus || {}; const diskStatus = longhornNode?.status?.diskStatus || {};
@ -144,10 +134,7 @@ export default {
storageTotal() { storageTotal() {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
const longhornNode = this.$store.getters[`${ inStore }/byId`]( const longhornNode = this.$store.getters[`${ inStore }/byId`](LONGHORN.NODES, `longhorn-system/${ this.value.id }`);
LONGHORN.NODES,
`longhorn-system/${ this.value.id }`
);
let out = 0; let out = 0;
const diskStatus = longhornNode?.status?.diskStatus || {}; const diskStatus = longhornNode?.status?.diskStatus || {};
@ -186,15 +173,8 @@ export default {
}, },
nodeRoleState() { nodeRoleState() {
const isExistRoleStatus = const isExistRoleStatus = this.value.metadata?.labels?.[HCI_ANNOTATIONS.NODE_ROLE_MASTER] !== undefined || this.value.metadata?.labels?.[HCI_ANNOTATIONS.NODE_ROLE_CONTROL_PLANE] !== undefined;
this.value.metadata?.labels?.[HCI_ANNOTATIONS.NODE_ROLE_MASTER] !== const promoteStatus = this.value.metadata?.annotations?.[HCI_ANNOTATIONS.PROMOTE_STATUS] || NONE;
undefined ||
this.value.metadata?.labels?.[
HCI_ANNOTATIONS.NODE_ROLE_CONTROL_PLANE
] !== undefined;
const promoteStatus =
this.value.metadata?.annotations?.[HCI_ANNOTATIONS.PROMOTE_STATUS] ||
NONE;
if (!isExistRoleStatus && promoteStatus === COMPLETE) { if (!isExistRoleStatus && promoteStatus === COMPLETE) {
return PROMOTE_RESTART; return PROMOTE_RESTART;
@ -227,7 +207,7 @@ export default {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.NODE_NETWORK); return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.NODE_NETWORK);
} },
}, },
methods: { methods: {
@ -241,7 +221,7 @@ export default {
}; };
return formatSi(value, formatOptions); return formatSi(value, formatOptions);
} },
} }
}; };
</script> </script>
@ -251,25 +231,16 @@ export default {
<h3>{{ t('harvester.host.tabs.overview') }}</h3> <h3>{{ t('harvester.host.tabs.overview') }}</h3>
<div class="row mb-20"> <div class="row mb-20">
<div class="col span-6"> <div class="col span-6">
<LabelValue <LabelValue :name="t('harvester.host.detail.customName')" :value="customName" />
:name="t('harvester.host.detail.customName')"
:value="customName"
/>
</div> </div>
<div class="col span-6"> <div class="col span-6">
<LabelValue <LabelValue :name="t('harvester.host.detail.hostIP')" :value="value.internalIp" />
:name="t('harvester.host.detail.hostIP')"
:value="value.internalIp"
/>
</div> </div>
</div> </div>
<div class="row mb-20"> <div class="row mb-20">
<div class="col span-6"> <div class="col span-6">
<LabelValue <LabelValue :name="t('harvester.host.detail.os')" :value="value.status.nodeInfo.osImage" />
:name="t('harvester.host.detail.os')"
:value="value.status.nodeInfo.osImage"
/>
</div> </div>
<div class="col span-6"> <div class="col span-6">
<div class="role"> <div class="role">
@ -287,28 +258,17 @@ export default {
<div class="row mb-20"> <div class="row mb-20">
<div class="col span-6"> <div class="col span-6">
<LabelValue <LabelValue :name="t('harvester.host.detail.create')" :value="value.metadata.creationTimestamp" />
:name="t('harvester.host.detail.create')"
:value="value.metadata.creationTimestamp"
/>
</div> </div>
<div class="col span-6"> <div class="col span-6">
<LabelValue <LabelValue :name="t('harvester.host.detail.update')" :value="lastUpdateTime" />
:name="t('harvester.host.detail.update')"
:value="lastUpdateTime"
/>
</div> </div>
</div> </div>
<div class="row mb-20"> <div class="row mb-20">
<div class="col span-6"> <div class="col span-6">
<LabelValue <LabelValue :name="t('harvester.host.detail.consoleUrl')" :value="consoleUrl.value">
:name="t('harvester.host.detail.consoleUrl')" <a slot="value" :href="consoleUrl.value" target="_blank">{{ consoleUrl.display }}</a>
:value="consoleUrl.value"
>
<a slot="value" :href="consoleUrl.value" target="_blank">{{
consoleUrl.display
}}</a>
</LabelValue> </LabelValue>
</div> </div>
</div> </div>
@ -321,10 +281,7 @@ export default {
</Banner> </Banner>
<div class="row mb-20"> <div class="row mb-20">
<div class="col span-6"> <div class="col span-6">
<LabelValue <LabelValue :name="t('harvester.host.detail.networkType')" :value="networkType" />
:name="t('harvester.host.detail.networkType')"
:value="networkType"
/>
</div> </div>
<div class="col span-6"> <div class="col span-6">
@ -365,24 +322,15 @@ export default {
<h3>{{ t('harvester.host.detail.more') }}</h3> <h3>{{ t('harvester.host.detail.more') }}</h3>
<div class="row mb-20"> <div class="row mb-20">
<div class="col span-4"> <div class="col span-4">
<LabelValue <LabelValue :name="t('harvester.host.detail.uuid')" :value="value.status.nodeInfo.systemUUID" />
:name="t('harvester.host.detail.uuid')"
:value="value.status.nodeInfo.systemUUID"
/>
</div> </div>
<div class="col span-4"> <div class="col span-4">
<LabelValue <LabelValue :name="t('harvester.host.detail.kernel')" :value="value.status.nodeInfo.kernelVersion" />
:name="t('harvester.host.detail.kernel')"
:value="value.status.nodeInfo.kernelVersion"
/>
</div> </div>
<div class="col span-4"> <div class="col span-4">
<LabelValue <LabelValue :name="t('harvester.host.detail.containerRuntime')" :value="value.status.nodeInfo.containerRuntimeVersion" />
:name="t('harvester.host.detail.containerRuntime')"
:value="value.status.nodeInfo.containerRuntimeVersion"
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,7 +2,7 @@
import { STATE, AGE, NAME } from '@shell/config/table-headers'; import { STATE, AGE, NAME } from '@shell/config/table-headers';
import SortableTable from '@shell/components/SortableTable'; import SortableTable from '@shell/components/SortableTable';
import Loading from '@shell/components/Loading'; import Loading from '@shell/components/Loading';
import HarvesterVmState from '@shell/components/formatter/HarvesterVmState'; import HarvesterVmState from '../../components/formatter/HarvesterVmState';
import { allHash } from '@shell/utils/promise'; import { allHash } from '@shell/utils/promise';
import { HCI } from '@shell/config/types'; import { HCI } from '@shell/config/types';
import { HOSTNAME } from '@shell/config/labels-annotations'; import { HOSTNAME } from '@shell/config/labels-annotations';
@ -13,22 +13,22 @@ export default {
components: { components: {
SortableTable, SortableTable,
Loading, Loading,
HarvesterVmState HarvesterVmState,
}, },
props: { props: {
node: { node: {
type: Object, type: Object,
required: true required: true,
} },
}, },
async fetch() { async fetch() {
const hash = await allHash({ const hash = await allHash({
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }), vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }), vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }),
allNodeNetwork: this.$store.dispatch('harvester/findAll', { type: HCI.NODE_NETWORK }), allNodeNetwork: this.$store.dispatch('harvester/findAll', { type: HCI.NODE_NETWORK }),
allClusterNetwork: this.$store.dispatch('harvester/findAll', { type: HCI.CLUSTER_NETWORK }) allClusterNetwork: this.$store.dispatch('harvester/findAll', { type: HCI.CLUSTER_NETWORK }),
}); });
const instanceMap = {}; const instanceMap = {};
@ -43,10 +43,7 @@ export default {
this.allNodeNetwork = hash.allNodeNetwork; this.allNodeNetwork = hash.allNodeNetwork;
this.allClusterNetwork = hash.allClusterNetwork; this.allClusterNetwork = hash.allClusterNetwork;
this.rows = hash.vms.filter((row) => { this.rows = hash.vms.filter((row) => {
return ( return instanceMap[row.metadata?.uid]?.status?.nodeName === this.node?.metadata?.labels?.[HOSTNAME];
instanceMap[row.metadata?.uid]?.status?.nodeName ===
this.node?.metadata?.labels?.[HOSTNAME]
);
}); });
}, },
@ -64,20 +61,20 @@ export default {
STATE, STATE,
NAME, NAME,
{ {
name: 'vmCPU', name: 'vmCPU',
labelKey: 'tableHeaders.cpu', labelKey: 'tableHeaders.cpu',
sort: 'vmCPU', sort: 'vmCPU',
search: false, search: false,
value: 'spec.template.spec.domain.cpu.cores', value: 'spec.template.spec.domain.cpu.cores',
width: 120 width: 120
}, },
{ {
name: 'vmRAM', name: 'vmRAM',
labelKey: 'glance.memory', labelKey: 'glance.memory',
sort: 'vmRAM', sort: 'vmRAM',
search: false, search: false,
value: 'spec.template.spec.domain.resources.limits.memory', value: 'spec.template.spec.domain.resources.limits.memory',
width: 120 width: 120
}, },
{ {
name: 'ip', name: 'ip',
@ -88,10 +85,10 @@ export default {
}, },
{ {
...AGE, ...AGE,
sort: 'metadata.creationTimestamp:desc' sort: 'metadata.creationTimestamp:desc',
} }
]; ];
} },
}, },
methods: {} methods: {}
@ -112,15 +109,10 @@ export default {
> >
<template slot="cell:state" slot-scope="scope" class="state-col"> <template slot="cell:state" slot-scope="scope" class="state-col">
<div class="state"> <div class="state">
<HarvesterVmState <HarvesterVmState class="vmstate" :row="scope.row" :all-node-network="allNodeNetwork" :all-cluster-network="allClusterNetwork" />
class="vmstate"
:row="scope.row"
:all-node-network="allNodeNetwork"
:all-cluster-network="allClusterNetwork"
/>
</div> </div>
</template> </template>
</SortableTable> </Sortabletable>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,5 +1,5 @@
<script> <script>
import HarvesterIpAddress from '@shell/components/formatter/HarvesterIpAddress'; import HarvesterIpAddress from '../../../components/formatter/HarvesterIpAddress';
import VMConsoleBar from '../../../components/VMConsoleBar'; import VMConsoleBar from '../../../components/VMConsoleBar';
import LabelValue from '@shell/components/LabelValue'; import LabelValue from '@shell/components/LabelValue';
import InputOrDisplay from '@shell/components/InputOrDisplay'; import InputOrDisplay from '@shell/components/InputOrDisplay';
@ -34,8 +34,8 @@ export default {
}, },
mode: { mode: {
type: String, type: String,
required: true required: true,
} },
}, },
computed: { computed: {
@ -46,8 +46,7 @@ export default {
return UNDEFINED; return UNDEFINED;
} }
return `${ date.getMonth() + return `${ date.getMonth() + 1 }/${ date.getDate() }/${ date.getUTCFullYear() }`;
1 }/${ date.getDate() }/${ date.getUTCFullYear() }`;
}, },
node() { node() {
@ -55,16 +54,13 @@ export default {
}, },
hostname() { hostname() {
return ( return this.resource?.spec?.hostname || this.resource?.status?.guestOSInfo?.hostname;
this.resource?.spec?.hostname ||
this.resource?.status?.guestOSInfo?.hostname
);
}, },
imageName() { imageName() {
const imageList = this.$store.getters['harvester/all'](HCI.IMAGE) || []; const imageList = this.$store.getters['harvester/all'](HCI.IMAGE) || [];
const image = imageList.find((I) => { const image = imageList.find( (I) => {
return this.value.rootImageId === I.id; return this.value.rootImageId === I.id;
}); });
@ -72,25 +68,21 @@ export default {
}, },
disks() { disks() {
const disks = const disks = this.value?.spec?.template?.spec?.domain?.devices?.disks || [];
this.value?.spec?.template?.spec?.domain?.devices?.disks || [];
return disks return disks.filter((disk) => {
.filter((disk) => { return !!disk.bootOrder;
return !!disk.bootOrder; }).sort((a, b) => {
}) if (a.bootOrder < b.bootOrder) {
.sort((a, b) => { return -1;
if (a.bootOrder < b.bootOrder) { }
return -1;
}
return 1; return 1;
}); });
}, },
cdroms() { cdroms() {
const disks = const disks = this.value?.spec?.template?.spec?.domain?.devices?.disks || [];
this.value?.spec?.template?.spec?.domain?.devices?.disks || [];
return disks.filter((disk) => { return disks.filter((disk) => {
return !!disk.cdrom; return !!disk.cdrom;
@ -100,9 +92,7 @@ export default {
flavor() { flavor() {
const domain = this.value?.spec?.template?.spec?.domain; const domain = this.value?.spec?.template?.spec?.domain;
return `${ domain.cpu?.cores } vCPU , ${ return `${ domain.cpu?.cores } vCPU , ${ domain.resources?.limits?.memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
domain.resources?.limits?.memory
} ${ this.t('harvester.virtualMachine.input.memory') }`;
}, },
kernelRelease() { kernelRelease() {
@ -118,9 +108,7 @@ export default {
}, },
machineType() { machineType() {
return ( return this.value?.spec?.template?.spec?.domain?.machine?.type || undefined;
this.value?.spec?.template?.spec?.domain?.machine?.type || undefined
);
} }
}, },
@ -143,15 +131,11 @@ export default {
<div class="overview-basics"> <div class="overview-basics">
<div class="row"> <div class="row">
<div class="col span-6"> <div class="col span-6">
<LabelValue <LabelValue :name="t('harvester.virtualMachine.detail.details.name')" :value="value.nameDisplay">
:name="t('harvester.virtualMachine.detail.details.name')"
:value="value.nameDisplay"
>
<template #value> <template #value>
<div class="smart-row"> <div class="smart-row">
<div class="console"> <div class="console">
{{ value.nameDisplay }} {{ value.nameDisplay }} <VMConsoleBar :resource="value" class="consoleBut" />
<VMConsoleBar :resource="value" class="consoleBut" />
</div> </div>
</div> </div>
</template> </template>
@ -165,35 +149,26 @@ export default {
<div class="row"> <div class="row">
<div class="col span-6"> <div class="col span-6">
<LabelValue <LabelValue :name="t('harvester.virtualMachine.detail.details.hostname')" :value="hostname">
:name="t('harvester.virtualMachine.detail.details.hostname')"
:value="hostname"
>
<template #value> <template #value>
<div v-if="!isDown"> <div v-if="!isDown">
{{ {{ hostname || t("harvester.virtualMachine.detail.GuestAgentNotInstalled") }}
hostname ||
t('harvester.virtualMachine.detail.GuestAgentNotInstalled')
}}
</div> </div>
<div v-else> <div v-else>
{{ t('harvester.virtualMachine.detail.details.down') }} {{ t("harvester.virtualMachine.detail.details.down") }}
</div> </div>
</template> </template>
</LabelValue> </LabelValue>
</div> </div>
<div class="col span-6"> <div class="col span-6">
<LabelValue <LabelValue :name="t('harvester.virtualMachine.detail.details.node')" :value="node">
:name="t('harvester.virtualMachine.detail.details.node')"
:value="node"
>
<template #value> <template #value>
<div v-if="!isDown"> <div v-if="!isDown">
{{ node }} {{ node }}
</div> </div>
<div v-else> <div v-else>
{{ t('harvester.virtualMachine.detail.details.down') }} {{ t("harvester.virtualMachine.detail.details.down") }}
</div> </div>
</template> </template>
</LabelValue> </LabelValue>
@ -202,9 +177,7 @@ export default {
<div class="row"> <div class="row">
<div class="col span-6"> <div class="col span-6">
<LabelValue <LabelValue :name="t('harvester.virtualMachine.detail.details.ipAddress')">
:name="t('harvester.virtualMachine.detail.details.ipAddress')"
>
<template #value> <template #value>
<HarvesterIpAddress v-model="value.id" :row="value" /> <HarvesterIpAddress v-model="value.id" :row="value" />
</template> </template>
@ -212,10 +185,7 @@ export default {
</div> </div>
<div class="col span-6"> <div class="col span-6">
<LabelValue <LabelValue :name="t('harvester.virtualMachine.detail.details.created')" :value="creationTimestamp" />
:name="t('harvester.virtualMachine.detail.details.created')"
:value="creationTimestamp"
/>
</div> </div>
</div> </div>
@ -225,37 +195,27 @@ export default {
<div class="row"> <div class="row">
<div class="col span-6"> <div class="col span-6">
<InputOrDisplay <InputOrDisplay :name="t('harvester.virtualMachine.detail.details.bootOrder')" :value="disks" :mode="mode">
:name="t('harvester.virtualMachine.detail.details.bootOrder')"
:value="disks"
:mode="mode"
>
<template #value> <template #value>
<ul> <ul>
<li v-for="disk in disks" :key="disk.bootOrder"> <li v-for="(disk) in disks" :key="disk.bootOrder">
{{ disk.bootOrder }}. {{ disk.name }} ({{ {{ disk.bootOrder }}. {{ disk.name }} ({{ getDeviceType(disk) }})
getDeviceType(disk)
}})
</li> </li>
</ul> </ul>
</template> </template>
</InputOrDisplay> </InputOrDisplay>
</div> </div>
<div class="col span-6"> <div class="col span-6">
<InputOrDisplay <InputOrDisplay :name="t('harvester.virtualMachine.detail.details.CDROMs')" :value="cdroms" :mode="mode">
:name="t('harvester.virtualMachine.detail.details.CDROMs')"
:value="cdroms"
:mode="mode"
>
<template #value> <template #value>
<div> <div>
<ul v-if="cdroms.length > 0"> <ul v-if="cdroms.length > 0">
<li v-for="rom in cdroms" :key="rom.name"> <li v-for="(rom) in cdroms" :key="rom.name">
{{ rom.name }} {{ rom.name }}
</li> </li>
</ul> </ul>
<span v-else> <span v-else>
{{ t('harvester.virtualMachine.detail.notAvailable') }} {{ t("harvester.virtualMachine.detail.notAvailable") }}
</span> </span>
</div> </div>
</template> </template>
@ -264,74 +224,56 @@ export default {
</div> </div>
<div class="row"> <div class="row">
<div class="col span-6"> <div class="col span-6">
<LabelValue <LabelValue :name="t('harvester.virtualMachine.detail.details.operatingSystem')" :value="operatingSystem || t('harvester.virtualMachine.detail.GuestAgentNotInstalled')" />
:name="t('harvester.virtualMachine.detail.details.operatingSystem')"
:value="
operatingSystem ||
t('harvester.virtualMachine.detail.GuestAgentNotInstalled')
"
/>
</div> </div>
<LabelValue <LabelValue :name="t('harvester.virtualMachine.detail.details.flavor')" :value="flavor" />
:name="t('harvester.virtualMachine.detail.details.flavor')"
:value="flavor"
/>
</div> </div>
<div class="row"> <div class="row">
<div class="col span-6"> <div class="col span-6">
<LabelValue <LabelValue :name="t('harvester.virtualMachine.detail.details.kernelRelease')" :value="kernelRelease || t('harvester.virtualMachine.detail.GuestAgentNotInstalled')" />
:name="t('harvester.virtualMachine.detail.details.kernelRelease')"
:value="
kernelRelease ||
t('harvester.virtualMachine.detail.GuestAgentNotInstalled')
"
/>
</div> </div>
<div class="col span-6"> <div class="col span-6">
<LabelValue <LabelValue :name="t('harvester.virtualMachine.input.MachineType')" :value="machineType" />
:name="t('harvester.virtualMachine.input.MachineType')"
:value="machineType"
/>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.consoleBut { .consoleBut {
position: relative; position: relative;
top: -20px; top: -20px;
left: 38px; left: 38px;
}
.overview-basics {
display: grid;
grid-template-columns: 100%;
grid-template-rows: auto;
grid-row-gap: 15px;
.badge-state {
padding: 2px 5px;
font-size: 12px;
margin-right: 3px;
} }
.smart-row { .overview-basics {
display: flex; display: grid;
flex-direction: row; grid-template-columns: 100%;
grid-template-rows: auto;
grid-row-gap: 15px;
.console { .badge-state {
padding: 2px 5px;
font-size: 12px;
margin-right: 3px;
}
.smart-row {
display: flex; display: flex;
flex-direction: row;
.console {
display: flex;
}
}
&__name {
flex: 1;
}
&__ssh-key {
min-width: 150px;
} }
} }
&__name {
flex: 1;
}
&__ssh-key {
min-width: 150px;
}
}
</style> </style>

View File

@ -11,14 +11,10 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
export default { export default {
name: 'HarvesterHotplugModal', name: 'HotplugModal',
components: { components: {
AsyncButton, AsyncButton, Card, LabeledInput, LabeledSelect, Banner
Card,
LabeledInput,
LabeledSelect,
Banner
}, },
props: { props: {
@ -37,7 +33,7 @@ export default {
diskName: '', diskName: '',
volumeName: '', volumeName: '',
errors: [], errors: [],
allPVCs: [] allPVCs: [],
}; };
}, },
@ -45,11 +41,7 @@ export default {
...mapGetters({ t: 'i18n/t' }), ...mapGetters({ t: 'i18n/t' }),
PVCs() { PVCs() {
return ( return this.allPVCs.filter(P => this.actionResource.metadata.namespace === P.metadata.namespace) || [];
this.allPVCs.filter(
P => this.actionResource.metadata.namespace === P.metadata.namespace
) || []
);
}, },
actionResource() { actionResource() {
@ -58,21 +50,23 @@ export default {
volumeOption() { volumeOption() {
return sortBy( return sortBy(
this.PVCs.filter((pvc) => { this.PVCs
if (!!pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) { .filter( (pvc) => {
return false; if (!!pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
} return false;
}
return !pvc.attachVM; return !pvc.attachVM;
}).map((pvc) => { })
return { .map((pvc) => {
label: pvc.metadata.name, return {
value: pvc.metadata.name label: pvc.metadata.name,
}; value: pvc.metadata.name
}), };
}),
'label' 'label'
); );
} },
}, },
methods: { methods: {
@ -85,25 +79,13 @@ export default {
async save(buttonCb) { async save(buttonCb) {
if (this.actionResource) { if (this.actionResource) {
try { try {
const res = await this.actionResource.doAction( const res = await this.actionResource.doAction('addVolume', { volumeSourceName: this.volumeName, diskName: this.diskName }, {}, false);
'addVolume',
{ volumeSourceName: this.volumeName, diskName: this.diskName },
{},
false
);
if (res._status === 200 || res._status === 204) { if (res._status === 200 || res._status === 204) {
this.$store.dispatch( this.$store.dispatch('growl/success', {
'growl/success', title: this.t('harvester.notification.title.succeed'),
{ message: this.t('harvester.modal.hotplug.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
title: this.t('harvester.notification.title.succeed'), }, { root: true });
message: this.t('harvester.modal.hotplug.success', {
diskName: this.diskName,
vm: this.actionResource.nameDisplay
})
},
{ root: true }
);
this.close(); this.close();
buttonCb(true); buttonCb(true);
@ -121,21 +103,21 @@ export default {
buttonCb(false); buttonCb(false);
} }
} }
} },
} }
}; };
</script> </script>
<template> <template>
<Card ref="modal" name="modal" :show-highlight-border="false"> <Card ref="modal" name="modal" :show-highlight-border="false">
<h4 <h4 slot="title" class="text-default-text" v-html="t('harvester.modal.hotplug.title')" />
slot="title"
class="text-default-text"
v-html="t('harvester.modal.hotplug.title')"
/>
<template #body> <template #body>
<LabeledInput v-model="diskName" :label="t('generic.name')" required /> <LabeledInput
v-model="diskName"
:label="t('generic.name')"
required
/>
<LabeledSelect <LabeledSelect
v-model="volumeName" v-model="volumeName"

View File

@ -10,7 +10,7 @@ import LabelValue from '@shell/components/LabelValue';
import Select from '@shell/components/form/Select'; import Select from '@shell/components/form/Select';
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import { OS } from '../mixins/harvester-vm'; import { OS } from '../mixins/harvester-vm';
import { VM_IMAGE_FILE_FORMAT } from '../validators/imageUrl'; import { VM_IMAGE_FILE_FORMAT } from '@shell/utils/validators/vm-image';
import { HCI as HCI_ANNOTATIONS } from '@shell/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@shell/config/labels-annotations';
import { exceptionToErrorsArray } from '@shell/utils/error'; import { exceptionToErrorsArray } from '@shell/utils/error';
@ -30,7 +30,7 @@ export default {
LabeledInput, LabeledInput,
NameNsDescription, NameNsDescription,
RadioGroup, RadioGroup,
LabelValue LabelValue,
}, },
mixins: [CreateEditView], mixins: [CreateEditView],
@ -38,12 +38,12 @@ export default {
props: { props: {
value: { value: {
type: Object, type: Object,
required: true required: true,
} },
}, },
data() { data() {
if (!this.value.spec) { if ( !this.value.spec ) {
this.$set(this.value, 'spec', { sourceType: DOWNLOAD }); this.$set(this.value, 'spec', { sourceType: DOWNLOAD });
} }
@ -52,12 +52,12 @@ export default {
} }
return { return {
url: this.value.spec.url, url: this.value.spec.url,
files: [], files: [],
resource: '', resource: '',
headers: {}, headers: {},
fileUrl: '', fileUrl: '',
file: '' file: '',
}; };
}, },
@ -67,9 +67,7 @@ export default {
}, },
imageName() { imageName() {
return ( return this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_NAME] || '-';
this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_NAME] || '-'
);
}, },
isCreateEdit() { isCreateEdit() {
@ -85,10 +83,7 @@ export default {
'value.spec.url'(neu) { 'value.spec.url'(neu) {
const url = neu.trim(); const url = neu.trim();
const suffixName = url.split('/').pop(); const suffixName = url.split('/').pop();
const fileSuffix = suffixName const fileSuffix = suffixName.split('.').pop().toLowerCase();
.split('.')
.pop()
.toLowerCase();
this.value.spec.url = url; this.value.spec.url = url;
if (VM_IMAGE_FILE_FORMAT.includes(fileSuffix)) { if (VM_IMAGE_FILE_FORMAT.includes(fileSuffix)) {
@ -106,7 +101,7 @@ export default {
if (!this.value.spec.displayName) { if (!this.value.spec.displayName) {
this.$refs.nd.changeNameAndNamespace({ this.$refs.nd.changeNameAndNamespace({
text: suffixName, text: suffixName,
selected: this.value.metadata.namespace selected: this.value.metadata.namespace,
}); });
} }
} }
@ -130,7 +125,7 @@ export default {
if (this.$refs?.file?.value) { if (this.$refs?.file?.value) {
this.$refs.file.value = null; this.$refs.file.value = null;
} }
} },
}, },
methods: { methods: {
@ -143,8 +138,7 @@ export default {
const file = this.file; const file = this.file;
this.value.metadata.annotations[HCI_ANNOTATIONS.IMAGE_NAME] = this.value.metadata.annotations[HCI_ANNOTATIONS.IMAGE_NAME] = file?.name;
file?.name;
const res = await this.value.save(); const res = await this.value.save();
@ -169,7 +163,7 @@ export default {
if (!this.value.spec.displayName) { if (!this.value.spec.displayName) {
this.$refs.nd.changeNameAndNamespace({ this.$refs.nd.changeNameAndNamespace({
text: file?.name, text: file?.name,
selected: this.value.metadata.namespace selected: this.value.metadata.namespace,
}); });
} }
}, },
@ -181,10 +175,7 @@ export default {
}, },
internalAnnotations(option) { internalAnnotations(option) {
const optionKeys = [ const optionKeys = [HCI_ANNOTATIONS.OS_TYPE, HCI_ANNOTATIONS.IMAGE_SUFFIX];
HCI_ANNOTATIONS.OS_TYPE,
HCI_ANNOTATIONS.IMAGE_SUFFIX
];
return optionKeys.find(O => O === option.key); return optionKeys.find(O => O === option.key);
}, },
@ -193,16 +184,13 @@ export default {
if (keyName === HCI_ANNOTATIONS.OS_TYPE) { if (keyName === HCI_ANNOTATIONS.OS_TYPE) {
return OS; return OS;
} else if (keyName === HCI_ANNOTATIONS.IMAGE_SUFFIX) { } else if (keyName === HCI_ANNOTATIONS.IMAGE_SUFFIX) {
return [ return [{
{ label: 'ISO',
label: 'ISO', value: 'iso'
value: 'iso' }, {
}, label: 'raw/qcow2',
{ value: rawORqcow2
label: 'raw/qcow2', }];
value: rawORqcow2
}
];
} }
return []; return [];
@ -217,16 +205,16 @@ export default {
return; return;
} }
return OS.find((os) => { return OS.find( (os) => {
if (os.match) { if (os.match) {
return os.match.find(matchValue => url.toLowerCase().includes(matchValue) return os.match.find(matchValue => url.toLowerCase().includes(matchValue)) ? os.value : false;
) ? os.value : false;
} else { } else {
return url.toLowerCase().includes(os.value.toLowerCase()) ? os.value : false; return url.toLowerCase().includes(os.value.toLowerCase()) ? os.value : false;
} }
}); });
} }
} },
}; };
</script> </script>
@ -249,20 +237,18 @@ export default {
/> />
<Tabbed v-bind="$attrs" class="mt-15" :side-tabs="true"> <Tabbed v-bind="$attrs" class="mt-15" :side-tabs="true">
<Tab <Tab name="basic" :label="t('harvester.image.tabs.basics')" :weight="3" class="bordered-table">
name="basic"
:label="t('harvester.image.tabs.basics')"
:weight="3"
class="bordered-table"
>
<RadioGroup <RadioGroup
v-if="isCreate" v-if="isCreate"
v-model="value.spec.sourceType" v-model="value.spec.sourceType"
name="model" name="model"
:options="['download', 'upload']" :options="[
'download',
'upload',
]"
:labels="[ :labels="[
t('harvester.image.sourceType.download'), t('harvester.image.sourceType.download'),
t('harvester.image.sourceType.upload') t('harvester.image.sourceType.upload'),
]" ]"
:mode="mode" :mode="mode"
/> />
@ -308,13 +294,19 @@ export default {
/> />
</button> </button>
<div v-if="uploadFileName" class="fileName mt-5"> <div
v-if="uploadFileName"
class="fileName mt-5"
>
<span class="icon icon-file" /> <span class="icon icon-file" />
{{ uploadFileName }} {{ uploadFileName }}
</div> </div>
</div> </div>
</div> </div>
<div v-else class="col span-12"> <div
v-else
class="col span-12"
>
<LabelValue <LabelValue
:name="t('harvester.image.fileName')" :name="t('harvester.image.fileName')"
:value="imageName" :value="imageName"
@ -323,12 +315,7 @@ export default {
</div> </div>
</Tab> </Tab>
<Tab <Tab name="labels" :label="t('labels.labels.title')" :weight="2" class="bordered-table">
name="labels"
:label="t('labels.labels.title')"
:weight="2"
class="bordered-table"
>
<KeyValue <KeyValue
key="labels" key="labels"
ref="labels" ref="labels"

View File

@ -2,7 +2,7 @@
import ConsoleBar from '../components/VMConsoleBar'; import ConsoleBar from '../components/VMConsoleBar';
import ResourceTable from '@shell/components/ResourceTable'; import ResourceTable from '@shell/components/ResourceTable';
import LinkDetail from '@shell/components/formatter/LinkDetail'; import LinkDetail from '@shell/components/formatter/LinkDetail';
import HarvesterVmState from '@shell/components/formatter/HarvesterVmState'; import HarvesterVmState from '../components/formatter/HarvesterVmState';
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers'; import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
import { HCI, NODE, POD } from '@shell/config/types'; import { HCI, NODE, POD } from '@shell/config/types';
@ -23,15 +23,15 @@ export default {
props: { props: {
schema: { schema: {
type: Object, type: Object,
required: true required: true,
} },
}, },
async fetch() { async fetch() {
const _hash = { const _hash = {
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }), vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
pod: this.$store.dispatch('harvester/findAll', { type: POD }), pod: this.$store.dispatch('harvester/findAll', { type: POD }),
restore: this.$store.dispatch('harvester/findAll', { type: HCI.RESTORE }) restore: this.$store.dispatch('harvester/findAll', { type: HCI.RESTORE }),
}; };
if (this.$store.getters['harvester/schemaFor'](NODE)) { if (this.$store.getters['harvester/schemaFor'](NODE)) {
@ -69,7 +69,7 @@ export default {
STATE, STATE,
{ {
...NAME, ...NAME,
width: 300 width: 300,
}, },
NAMESPACE, NAMESPACE,
{ {
@ -78,7 +78,7 @@ export default {
sort: ['spec.template.spec.domain.cpu.cores'], sort: ['spec.template.spec.domain.cpu.cores'],
value: 'spec.template.spec.domain.cpu.cores', value: 'spec.template.spec.domain.cpu.cores',
align: 'center', align: 'center',
dashIfEmpty: true dashIfEmpty: true,
}, },
{ {
name: 'Memory', name: 'Memory',
@ -89,14 +89,10 @@ export default {
formatter: 'Si', formatter: 'Si',
formatterOpts: { formatterOpts: {
opts: { opts: {
increment: 1024, increment: 1024, addSuffix: true, maxExponent: 3, minExponent: 3, suffix: 'i',
addSuffix: true,
maxExponent: 3,
minExponent: 3,
suffix: 'i'
}, },
needParseSi: true needParseSi: true
} },
}, },
{ {
name: 'ip', name: 'ip',
@ -115,15 +111,13 @@ export default {
}, },
{ {
...AGE, ...AGE,
sort: 'metadata.creationTimestamp:desc' sort: 'metadata.creationTimestamp:desc',
} }
]; ];
}, },
rows() { rows() {
const matchVMIs = this.allVMIs.filter( const matchVMIs = this.allVMIs.filter(VMI => !this.allVMs.find(VM => VM.id === VMI.id));
VMI => !this.allVMs.find(VM => VM.id === VMI.id)
);
return [...this.allVMs, ...matchVMIs]; return [...this.allVMs, ...matchVMIs];
} }
@ -154,22 +148,13 @@ export default {
> >
<template slot="cell:state" slot-scope="scope" class="state-col"> <template slot="cell:state" slot-scope="scope" class="state-col">
<div class="state"> <div class="state">
<HarvesterVmState <HarvesterVmState class="vmstate" :row="scope.row" :all-node-network="allNodeNetworks" :all-cluster-network="allClusterNetworks" />
class="vmstate"
:row="scope.row"
:all-node-network="allNodeNetworks"
:all-cluster-network="allClusterNetworks"
/>
</div> </div>
</template> </template>
<template slot="cell:name" slot-scope="scope"> <template slot="cell:name" slot-scope="scope">
<div class="name-console"> <div class="name-console">
<LinkDetail <LinkDetail v-if="scope.row.type !== HCI.VMI" v-model="scope.row.metadata.name" :row="scope.row" />
v-if="scope.row.type !== HCI.VMI"
v-model="scope.row.metadata.name"
:row="scope.row"
/>
<span v-else> <span v-else>
{{ scope.row.metadata.name }} {{ scope.row.metadata.name }}
</span> </span>
@ -197,12 +182,17 @@ export default {
span { span {
line-height: 26px; line-height: 26px;
width: 160px; width:160px;
overflow: hidden; overflow:hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
-o-text-overflow: ellipsis; -o-text-overflow:ellipsis;
} }
<<<<<<< HEAD
=======
display: flex;
justify-content: space-around
>>>>>>> fix conflict
} }
</style> </style>

View File

@ -1,11 +0,0 @@
import { HCI } from '@shell/config/labels-annotations';
export default function fileRequired(annotations = {}, getters, errors, validatorArgs, type) {
const t = getters['i18n/t'];
if (!annotations[HCI.IMAGE_NAME]) {
errors.push(t('validation.required', { key: t('harvester.image.fileName') }));
}
return errors;
}

View File

@ -1,24 +0,0 @@
export const VM_IMAGE_FILE_FORMAT = ['qcow', 'qcow2', 'raw', 'img', 'iso'];
export default function imageUrl(url, getters, errors, validatorArgs, type) {
const t = getters['i18n/t'];
if (!url || url === '') {
return errors;
}
const suffixName = url.split('/').pop();
const fileSuffix = suffixName
.split('.')
.pop()
.toLowerCase();
if (!VM_IMAGE_FILE_FORMAT.includes(fileSuffix)) {
const tipString =
type === 'file' ? 'harvester.validation.image.ruleFileTip' : 'harvester.validation.image.ruleTip';
errors.push(t(tipString));
}
return errors;
}

View File

@ -1,70 +0,0 @@
export default function vmNetworks(spec, getters, errors, validatorArgs) {
const { domain: { devices: { interfaces } }, networks } = spec;
const allNames = new Set();
interfaces.map( (I, index) => {
allNames.add(I.name);
const N = networks.find( N => I.name === N.name);
const prefix = (I.name || N.name) || index + 1;
if (I.name.length > 20) {
const message = getters['i18n/t']('harvester.validation.custom.tooLongName', { max: 20 });
errors.push(getters['i18n/t']('harvester.validation.vm.network.error', { prefix, message }));
}
if (!I.name || !N.name) {
const message = getters['i18n/t']('harvester.validation.vm.name');
errors.push(getters['i18n/t']('harvester.validation.vm.network.error', { prefix, message }));
}
if (N.multus) {
if (!N.multus.networkName) {
const message = getters['i18n/t']('harvester.validation.vm.network.name');
errors.push(getters['i18n/t']('harvester.validation.vm.network.error', { prefix, message }));
}
}
if (I.macAddress && !isValidMac(I.macAddress) && !N.pod) {
const message = getters['i18n/t']('harvester.validation.vm.network.macFormat');
errors.push(getters['i18n/t']('harvester.validation.vm.network.error', { prefix, message }));
}
const portsName = new Set();
const portsNumber = new Set();
if (I.masquerade && I.ports) {
const ports = I?.ports || [];
ports.forEach((P) => {
portsName.add(P.name);
portsNumber.add(P.port);
});
if (portsName.size !== I.ports.length) {
const message = getters['i18n/t']('harvester.validation.vm.network.duplicatedPortName');
errors.push(getters['i18n/t']('harvester.validation.vm.network.error', { prefix, message }));
}
if (portsNumber.size !== I.ports.length) {
const message = getters['i18n/t']('harvester.validation.vm.network.duplicatedPortNumber');
errors.push(getters['i18n/t']('harvester.validation.vm.network.error', { prefix, message }));
}
}
});
if (allNames.size !== interfaces.length) {
errors.push(getters['i18n/t']('harvester.validation.vm.network.duplicatedName'));
}
return errors;
}
function isValidMac(value) {
return /^[A-Fa-f0-9]{2}(-[A-Fa-f0-9]{2}){5}$|^[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){5}$/.test(value);
}

View File

@ -22,6 +22,7 @@ export class Plugin implements IPlugin {
// Plugin metadata (plugin package.json) // Plugin metadata (plugin package.json)
public _metadata: any = {}; public _metadata: any = {};
public _validators: { [key: string]: Function } = {};
// Is this a built-in plugin (bundled with the application) // Is this a built-in plugin (bundled with the application)
public builtin = false; public builtin = false;
@ -43,6 +44,14 @@ export class Plugin implements IPlugin {
this.name = this._metadata.name || this.id; this.name = this._metadata.name || this.id;
} }
get validators() {
return this._validators;
}
set validators(vals:{ [key: string]: Function }) {
this._validators = vals;
}
// Track which products the plugin creates // Track which products the plugin creates
DSL(store: any, productName: string) { DSL(store: any, productName: string) {
const storeDSL = STORE_DSL(store, productName); const storeDSL = STORE_DSL(store, productName);

View File

@ -1,6 +1,6 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const contextFolders = ['chart', 'cloud-credential', 'content', 'detail', 'edit', 'list', 'machine-config', 'models', 'promptRemove', 'l10n', 'windowComponents', 'dialog', 'validators']; const contextFolders = ['chart', 'cloud-credential', 'content', 'detail', 'edit', 'list', 'machine-config', 'models', 'promptRemove', 'l10n', 'windowComponents', 'dialog'];
const contextMap = contextFolders.reduce((map, obj) => { const contextMap = contextFolders.reduce((map, obj) => {
map[obj] = true; map[obj] = true;

File diff suppressed because it is too large Load Diff

View File

@ -409,25 +409,6 @@ export const getters = {
if (state.isSingleProduct !== undefined) { if (state.isSingleProduct !== undefined) {
return state.isSingleProduct; return state.isSingleProduct;
} }
// TODO not this
if (rootGetters.isSingleVirtualCluster) {
// return {
// logo: require('~shell/assets/images/providers/harvester.svg'),
// productNameKey: 'product.harvester',
// version: rootGetters['harvester/byId'](HCI.SETTING, 'server-version')?.value,
// afterLoginRoute: {
// name: 'c-cluster-product',
// params: { product: VIRTUAL },
// },
// logoRoute: {
// name: 'c-cluster-product-resource',
// params: {
// product: VIRTUAL,
// resource: HCI.DASHBOARD,
// }
// },
// };
}
return false; return false;
}, },

View File

@ -798,7 +798,13 @@ export const getters = {
allTypes(state, getters, rootState, rootGetters) { allTypes(state, getters, rootState, rootGetters) {
return (product, mode = ALL) => { return (product, mode = ALL) => {
const module = findBy(state.products, 'name', product).inStore; let module;
try {
module = findBy(state.products, 'name', product).inStore;
} catch {
debugger;
}
const schemas = rootGetters[`${ module }/all`](SCHEMA); const schemas = rootGetters[`${ module }/all`](SCHEMA);
const counts = rootGetters[`${ module }/all`](COUNT)?.[0]?.counts || {}; const counts = rootGetters[`${ module }/all`](COUNT)?.[0]?.counts || {};
const isDev = rootGetters['prefs/get'](DEV); const isDev = rootGetters['prefs/get'](DEV);

View File

@ -1,14 +1,7 @@
import { flowOutput } from '@shell/utils/validators/flow-output'; import { flowOutput } from '@shell/utils/validators/flow-output';
import { logdna } from '@shell/utils/validators/logging-outputs'; import { logdna } from '@shell/utils/validators/logging-outputs';
import { import { clusterIp, externalName, servicePort } from '@shell/utils/validators/service';
clusterIp, import { ruleGroups, groupsAreValid } from '@shell/utils/validators/prometheusrule';
externalName,
servicePort
} from '@shell/utils/validators/service';
import {
ruleGroups,
groupsAreValid
} from '@shell/utils/validators/prometheusrule';
import { interval, matching } from '@shell/utils/validators/monitoring-route'; import { interval, matching } from '@shell/utils/validators/monitoring-route';
import { containerImages } from '@shell/utils/validators/container-images'; import { containerImages } from '@shell/utils/validators/container-images';
import { cronSchedule } from '@shell/utils/validators/cron-schedule'; import { cronSchedule } from '@shell/utils/validators/cron-schedule';
@ -17,11 +10,16 @@ import { roleTemplateRules } from '@shell/utils/validators/role-template';
import { clusterName } from '@shell/utils/validators/cluster-name'; import { clusterName } from '@shell/utils/validators/cluster-name';
import { isHttps, backupTarget } from '@shell/utils/validators/setting'; import { isHttps, backupTarget } from '@shell/utils/validators/setting';
import { imageUrl, fileRequired } from '@shell/utils/validators/vm-image';
import { vmNetworks, vmDisks } from '@shell/utils/validators/vm';
import { dataVolumeSize } from '@shell/utils/validators/vm-datavolumes';
/** /**
* Custom validation functions beyond normal scalr types * Custom validation functions beyond normal scalr types
* Validator must export a function name should match the validator name on the customValidationRules rule * Validator must export a function name should match the validator name on the customValidationRules rule
* Exported function is used as a lookup key in resource-class:validationErrors:customValidationRules loop * Exported function is used as a lookup key in resource-class:validationErrors:customValidationRules loop
*/ */
export default { export default {
clusterName, clusterName,
clusterIp, clusterIp,
@ -38,5 +36,10 @@ export default {
podAffinity, podAffinity,
roleTemplateRules, roleTemplateRules,
isHttps, isHttps,
backupTarget backupTarget,
imageUrl,
dataVolumeSize,
vmNetworks,
vmDisks,
fileRequired,
}; };

View File

@ -1,6 +1,6 @@
import { formatSi, parseSi } from '@shell/utils/units'; import { formatSi, parseSi } from '@shell/utils/units';
export default function dataVolumeSize(storage, getters, errors, validatorArgs) { export function dataVolumeSize(storage, getters, errors, validatorArgs) {
const t = getters['i18n/t']; const t = getters['i18n/t'];
if (!storage || storage === '') { if (!storage || storage === '') {

View File

@ -0,0 +1,32 @@
import { HCI } from '@shell/config/labels-annotations';
export const VM_IMAGE_FILE_FORMAT = ['qcow', 'qcow2', 'raw', 'img', 'iso'];
export function imageUrl(url, getters, errors, validatorArgs, type) {
const t = getters['i18n/t'];
if (!url || url === '') {
return errors;
}
const suffixName = url.split('/').pop();
const fileSuffix = suffixName.split('.').pop().toLowerCase();
if (!VM_IMAGE_FILE_FORMAT.includes(fileSuffix)) {
const tipString = type === 'file' ? 'harvester.validation.image.ruleFileTip' : 'harvester.validation.image.ruleTip';
errors.push(t(tipString));
}
return errors;
}
export function fileRequired(annotations = {}, getters, errors, validatorArgs, type) {
const t = getters['i18n/t'];
if (!annotations[HCI.IMAGE_NAME]) {
errors.push(t('validation.required', { key: t('harvester.image.fileName') }));
}
return errors;
}

View File

@ -2,7 +2,74 @@ import { PVC } from '@shell/config/types';
import { SOURCE_TYPE } from '@shell/config/harvester-map'; import { SOURCE_TYPE } from '@shell/config/harvester-map';
import { HCI as HCI_ANNOTATIONS } from '@shell/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@shell/config/labels-annotations';
export default function vmDisks(spec, getters, errors, validatorArgs, displayKey, value) { export function vmNetworks(spec, getters, errors, validatorArgs) {
const { domain: { devices: { interfaces } }, networks } = spec;
const allNames = new Set();
interfaces.map( (I, index) => {
allNames.add(I.name);
const N = networks.find( N => I.name === N.name);
const prefix = (I.name || N.name) || index + 1;
if (I.name.length > 20) {
const message = getters['i18n/t']('harvester.validation.custom.tooLongName', { max: 20 });
errors.push(getters['i18n/t']('harvester.validation.vm.network.error', { prefix, message }));
}
if (!I.name || !N.name) {
const message = getters['i18n/t']('harvester.validation.vm.name');
errors.push(getters['i18n/t']('harvester.validation.vm.network.error', { prefix, message }));
}
if (N.multus) {
if (!N.multus.networkName) {
const message = getters['i18n/t']('harvester.validation.vm.network.name');
errors.push(getters['i18n/t']('harvester.validation.vm.network.error', { prefix, message }));
}
}
if (I.macAddress && !isValidMac(I.macAddress) && !N.pod) {
const message = getters['i18n/t']('harvester.validation.vm.network.macFormat');
errors.push(getters['i18n/t']('harvester.validation.vm.network.error', { prefix, message }));
}
const portsName = new Set();
const portsNumber = new Set();
if (I.masquerade && I.ports) {
const ports = I?.ports || [];
ports.forEach((P) => {
portsName.add(P.name);
portsNumber.add(P.port);
});
if (portsName.size !== I.ports.length) {
const message = getters['i18n/t']('harvester.validation.vm.network.duplicatedPortName');
errors.push(getters['i18n/t']('harvester.validation.vm.network.error', { prefix, message }));
}
if (portsNumber.size !== I.ports.length) {
const message = getters['i18n/t']('harvester.validation.vm.network.duplicatedPortNumber');
errors.push(getters['i18n/t']('harvester.validation.vm.network.error', { prefix, message }));
}
}
});
if (allNames.size !== interfaces.length) {
errors.push(getters['i18n/t']('harvester.validation.vm.network.duplicatedName'));
}
return errors;
}
export function vmDisks(spec, getters, errors, validatorArgs, displayKey, value) {
const isVMTemplate = validatorArgs.includes('isVMTemplate'); const isVMTemplate = validatorArgs.includes('isVMTemplate');
const data = isVMTemplate ? this.value.spec.vm : value; const data = isVMTemplate ? this.value.spec.vm : value;
@ -109,7 +176,11 @@ export default function vmDisks(spec, getters, errors, validatorArgs, displayKey
return errors; return errors;
} }
function getVolumeType(V, DVTS) { export function isValidMac(value) {
return /^[A-Fa-f0-9]{2}(-[A-Fa-f0-9]{2}){5}$|^[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){5}$/.test(value);
}
export function getVolumeType(V, DVTS) {
let outValue = null; let outValue = null;
if (V.persistentVolumeClaim) { if (V.persistentVolumeClaim) {