mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-05-15 23:41:43 +00:00
Compare commits
39 Commits
main
...
v1.8.1-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b887e5ee01 | ||
|
|
bf9b5866a7 | ||
|
|
b97017e076 | ||
|
|
3740c9c424 | ||
|
|
af8e8b2eeb | ||
|
|
7906033287 | ||
|
|
5584c86789 | ||
|
|
1dcb528717 | ||
|
|
36fc21ccbc | ||
|
|
8803b79524 | ||
|
|
3f80a71303 | ||
|
|
13e4154335 | ||
|
|
a80b87ce4f | ||
|
|
809b9925fa | ||
|
|
7788161d94 | ||
|
|
90b8a821a1 | ||
|
|
87ff014bd8 | ||
|
|
20328b06e6 | ||
|
|
bbc415a2b4 | ||
|
|
c12d4b5fba | ||
|
|
dc91be11f9 | ||
|
|
e5b72499b5 | ||
|
|
e66037b007 | ||
|
|
c676cad057 | ||
|
|
254abd4648 | ||
|
|
1df1d3534a | ||
|
|
ff1e6e6e7f | ||
|
|
622242e4a5 | ||
|
|
d8ea9be174 | ||
|
|
f8a479cfcf | ||
|
|
6d5e584113 | ||
|
|
a9eb04195e | ||
|
|
0441c1f6e3 | ||
|
|
dde58269d5 | ||
|
|
b8111f0ad7 | ||
|
|
6d627f82e9 | ||
|
|
cfc7a76fe7 | ||
|
|
71d3067354 | ||
|
|
9ecc372009 |
17
.github/renovate.json
vendored
17
.github/renovate.json
vendored
@ -39,20 +39,11 @@
|
|||||||
"reviewers": ["a110605", "houhoucoop"]
|
"reviewers": ["a110605", "houhoucoop"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchUpdateTypes": ["patch"],
|
"matchUpdateTypes": ["patch", "digest"],
|
||||||
"automerge": false,
|
"automerge": true,
|
||||||
"minimumReleaseAge": "7 days",
|
"minimumReleaseAge": "7 days",
|
||||||
"groupName": "patch dependencies",
|
"groupName": "patch digest dependencies",
|
||||||
"labels": ["patch-update"],
|
"labels": ["patch-update", "automerge"]
|
||||||
"reviewers": ["a110605", "houhoucoop"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matchUpdateTypes": ["digest", "pinDigest"],
|
|
||||||
"automerge": false,
|
|
||||||
"groupName": "digest dependencies",
|
|
||||||
"labels": ["digest-update"],
|
|
||||||
"schedule": ["on the first day of the month"],
|
|
||||||
"reviewers": ["a110605", "houhoucoop"]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.9.0-dev",
|
"version": "1.8.1-dev",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.0.0"
|
"node": ">=24.0.0"
|
||||||
|
|||||||
@ -122,7 +122,7 @@ export default {
|
|||||||
return this.$store.getters['currentCluster'].isLocal;
|
return this.$store.getters['currentCluster'].isLocal;
|
||||||
},
|
},
|
||||||
canEditClusterMembers() {
|
canEditClusterMembers() {
|
||||||
return this.schema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
|
return this.normanClusterRTBSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,196 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import { NAMESPACE } from '@shell/config/types';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterClusterPodSecurityStandard',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
RadioGroup,
|
|
||||||
LabeledSelect,
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
await this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE });
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
let enabled = false;
|
|
||||||
let whitelistedNamespaces = [];
|
|
||||||
let privilegedNamespaces = [];
|
|
||||||
let restrictedNamespaces = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(this.value.value || this.value.default || '{}');
|
|
||||||
|
|
||||||
enabled = !!parsed.enabled;
|
|
||||||
whitelistedNamespaces = parsed.whitelistedNamespacesList ? parsed.whitelistedNamespacesList.split(',') : [];
|
|
||||||
privilegedNamespaces = parsed.privilegedNamespacesList ? parsed.privilegedNamespacesList.split(',') : [];
|
|
||||||
restrictedNamespaces = parsed.restrictedNamespacesList ? parsed.restrictedNamespacesList.split(',') : [];
|
|
||||||
} catch (e) {
|
|
||||||
enabled = false;
|
|
||||||
whitelistedNamespaces = [];
|
|
||||||
privilegedNamespaces = [];
|
|
||||||
restrictedNamespaces = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabled,
|
|
||||||
whitelistedNamespaces,
|
|
||||||
privilegedNamespaces,
|
|
||||||
restrictedNamespaces,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
enabledOptions() {
|
|
||||||
return [
|
|
||||||
{ label: this.t('generic.enabled'), value: true },
|
|
||||||
{ label: this.t('generic.disabled'), value: false },
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
allNamespaces() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
return this.$store.getters[`${ inStore }/all`](NAMESPACE).filter((ns) => !ns.isSystem).map((ns) => ns.id);
|
|
||||||
},
|
|
||||||
|
|
||||||
whitelistedOptions() {
|
|
||||||
const excluded = new Set([...this.privilegedNamespaces, ...this.restrictedNamespaces]);
|
|
||||||
|
|
||||||
return this.allNamespaces.filter((ns) => !excluded.has(ns));
|
|
||||||
},
|
|
||||||
|
|
||||||
privilegedOptions() {
|
|
||||||
const excluded = new Set([...this.whitelistedNamespaces, ...this.restrictedNamespaces]);
|
|
||||||
|
|
||||||
return this.allNamespaces.filter((ns) => !excluded.has(ns));
|
|
||||||
},
|
|
||||||
|
|
||||||
restrictedOptions() {
|
|
||||||
const excluded = new Set([...this.whitelistedNamespaces, ...this.privilegedNamespaces]);
|
|
||||||
|
|
||||||
return this.allNamespaces.filter((ns) => !excluded.has(ns));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
useDefault() {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(this.value.default || '{}');
|
|
||||||
|
|
||||||
this.enabled = !!parsed.enabled;
|
|
||||||
this.whitelistedNamespaces = parsed.whitelistedNamespacesList ? parsed.whitelistedNamespacesList.split(',') : [];
|
|
||||||
this.privilegedNamespaces = parsed.privilegedNamespacesList ? parsed.privilegedNamespacesList.split(',') : [];
|
|
||||||
this.restrictedNamespaces = parsed.restrictedNamespacesList ? parsed.restrictedNamespacesList.split(',') : [];
|
|
||||||
} catch (e) {
|
|
||||||
this.enabled = false;
|
|
||||||
this.whitelistedNamespaces = [];
|
|
||||||
this.privilegedNamespaces = [];
|
|
||||||
this.restrictedNamespaces = [];
|
|
||||||
}
|
|
||||||
this.save();
|
|
||||||
},
|
|
||||||
|
|
||||||
onUpdateEnabled() {
|
|
||||||
if (!this.enabled) {
|
|
||||||
this.whitelistedNamespaces = [];
|
|
||||||
this.privilegedNamespaces = [];
|
|
||||||
this.restrictedNamespaces = [];
|
|
||||||
}
|
|
||||||
this.save();
|
|
||||||
},
|
|
||||||
|
|
||||||
updateWhitelisted(selected) {
|
|
||||||
this.whitelistedNamespaces = selected;
|
|
||||||
this.save();
|
|
||||||
},
|
|
||||||
|
|
||||||
updatePrivileged(selected) {
|
|
||||||
this.privilegedNamespaces = selected;
|
|
||||||
this.save();
|
|
||||||
},
|
|
||||||
|
|
||||||
updateRestricted(selected) {
|
|
||||||
this.restrictedNamespaces = selected;
|
|
||||||
this.save();
|
|
||||||
},
|
|
||||||
|
|
||||||
save() {
|
|
||||||
this.value.value = JSON.stringify({
|
|
||||||
enabled: this.enabled,
|
|
||||||
whitelistedNamespacesList: this.whitelistedNamespaces.join(','),
|
|
||||||
privilegedNamespacesList: this.privilegedNamespaces.join(','),
|
|
||||||
restrictedNamespacesList: this.restrictedNamespaces.join(','),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<RadioGroup
|
|
||||||
v-model:value="enabled"
|
|
||||||
name="enabled"
|
|
||||||
:options="enabledOptions"
|
|
||||||
@update:value="onUpdateEnabled"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template v-if="enabled">
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="whitelistedNamespaces"
|
|
||||||
:label="t('harvester.setting.clusterPodSecurityStandard.whitelistedNamespaces.label')"
|
|
||||||
:options="whitelistedOptions"
|
|
||||||
:multiple="true"
|
|
||||||
:mode="mode"
|
|
||||||
@update:value="updateWhitelisted"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="privilegedNamespaces"
|
|
||||||
:label="t('harvester.setting.clusterPodSecurityStandard.privilegedNamespaces.label')"
|
|
||||||
:options="privilegedOptions"
|
|
||||||
:multiple="true"
|
|
||||||
:mode="mode"
|
|
||||||
@update:value="updatePrivileged"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="restrictedNamespaces"
|
|
||||||
:label="t('harvester.setting.clusterPodSecurityStandard.restrictedNamespaces.label')"
|
|
||||||
:options="restrictedOptions"
|
|
||||||
:multiple="true"
|
|
||||||
:mode="mode"
|
|
||||||
@update:value="updateRestricted"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -65,11 +65,8 @@ const FEATURE_FLAGS = {
|
|||||||
'vGPUAsPCIDevice',
|
'vGPUAsPCIDevice',
|
||||||
'instanceManagerResourcesSetting',
|
'instanceManagerResourcesSetting',
|
||||||
'rwxNetworkSetting',
|
'rwxNetworkSetting',
|
||||||
'createPVCWithDataVolume',
|
'createPVCWithDataVolume'
|
||||||
'clusterPodSecurityStandardSetting'
|
|
||||||
],
|
],
|
||||||
'v1.8.1': [],
|
|
||||||
'v1.9.0': [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateFeatureFlags = () => {
|
const generateFeatureFlags = () => {
|
||||||
|
|||||||
@ -686,7 +686,7 @@ export function init($plugin, store) {
|
|||||||
},
|
},
|
||||||
resource: NETWORK_ATTACHMENT,
|
resource: NETWORK_ATTACHMENT,
|
||||||
resourceDetail: HCI.NETWORK_ATTACHMENT,
|
resourceDetail: HCI.NETWORK_ATTACHMENT,
|
||||||
resourceEdit: HCI.NETWORK_ATTACHMENT,
|
resourceEdit: HCI.NETWORK_ATTACHMENT
|
||||||
});
|
});
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
|
|||||||
@ -41,8 +41,7 @@ export const HCI_SETTING = {
|
|||||||
RANCHER_CLUSTER: 'rancher-cluster',
|
RANCHER_CLUSTER: 'rancher-cluster',
|
||||||
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
|
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
|
||||||
KUBEVIRT_MIGRATION: 'kubevirt-migration',
|
KUBEVIRT_MIGRATION: 'kubevirt-migration',
|
||||||
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources',
|
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources'
|
||||||
CLUSTER_POD_SECURITY_STANDARD: 'cluster-pod-security-standard'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_ALLOWED_SETTINGS = {
|
export const HCI_ALLOWED_SETTINGS = {
|
||||||
@ -131,9 +130,6 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
},
|
},
|
||||||
[HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: {
|
[HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: {
|
||||||
kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting'
|
kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting'
|
||||||
},
|
|
||||||
[HCI_SETTING.CLUSTER_POD_SECURITY_STANDARD]: {
|
|
||||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'clusterPodSecurityStandardSetting'
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,154 +0,0 @@
|
|||||||
<script>
|
|
||||||
import merge from 'lodash/merge';
|
|
||||||
import jsyaml from 'js-yaml';
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { Card } from '@components/Card';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
|
||||||
import { escapeHtml } from '@shell/utils/string';
|
|
||||||
|
|
||||||
const DEFAULT_VALUE = { image: { repository: 'rancher/harvester-nvidia-driver-toolkit' } };
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterEnableNvidiaDriverToolkit',
|
|
||||||
|
|
||||||
emits: ['close'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
AsyncButton,
|
|
||||||
Card,
|
|
||||||
LabeledInput,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
resources: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
const addon = this.resources[0];
|
|
||||||
let valuesContentJson;
|
|
||||||
|
|
||||||
try {
|
|
||||||
valuesContentJson = merge({}, DEFAULT_VALUE, jsyaml.load(addon.spec.valuesContent));
|
|
||||||
} catch (e) {
|
|
||||||
valuesContentJson = { ...DEFAULT_VALUE };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { valuesContentJson };
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
buttonDisabled() {
|
|
||||||
const { image, driverLocation } = this.valuesContentJson;
|
|
||||||
|
|
||||||
return !(image?.repository || '').trim() || !(image?.tag || '').trim() || !(driverLocation || '').trim();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.$emit('close');
|
|
||||||
},
|
|
||||||
|
|
||||||
async enable(buttonCb) {
|
|
||||||
const addon = this.resources[0];
|
|
||||||
|
|
||||||
try {
|
|
||||||
addon.spec.valuesContent = jsyaml.dump(this.valuesContentJson);
|
|
||||||
addon.spec.enabled = true;
|
|
||||||
await addon.save();
|
|
||||||
buttonCb(true);
|
|
||||||
this.close();
|
|
||||||
} catch (err) {
|
|
||||||
addon.spec.enabled = false;
|
|
||||||
this.$store.dispatch('growl/fromError', {
|
|
||||||
title: this.t('generic.notification.title.error', { name: escapeHtml(addon.metadata.name) }),
|
|
||||||
err,
|
|
||||||
}, { root: true });
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Card :show-highlight-border="false">
|
|
||||||
<template #title>
|
|
||||||
<h4
|
|
||||||
v-clean-html="t('harvester.addons.nvidiaDriverToolkit.enable.title')"
|
|
||||||
class="text-default-text"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<div class="body">
|
|
||||||
<div class="row mb-15">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="valuesContentJson.image.repository"
|
|
||||||
:required="true"
|
|
||||||
:label="t('harvester.addons.nvidiaDriverToolkit.image.repository')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="valuesContentJson.image.tag"
|
|
||||||
:required="true"
|
|
||||||
:label="t('harvester.addons.nvidiaDriverToolkit.image.tag')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-15">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="valuesContentJson.driverLocation"
|
|
||||||
:required="true"
|
|
||||||
:label="t('harvester.addons.nvidiaDriverToolkit.driver.location')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #actions>
|
|
||||||
<div class="buttons actions">
|
|
||||||
<button
|
|
||||||
class="btn role-secondary mr-10"
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
{{ t('generic.cancel') }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<AsyncButton
|
|
||||||
mode="enable"
|
|
||||||
:disabled="buttonDisabled"
|
|
||||||
@click="enable"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,12 +1,15 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
import { NODE } from '@shell/config/types';
|
import { NODE } from '@shell/config/types';
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
|
||||||
import { Card } from '@components/Card';
|
import { Card } from '@components/Card';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
@ -59,46 +62,28 @@ export default {
|
|||||||
return this.resources[0];
|
return this.resources[0];
|
||||||
},
|
},
|
||||||
|
|
||||||
anyCpuPinning() {
|
vmi() {
|
||||||
return this.resources.some((r) => r.isCpuPinning);
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
},
|
const vmiResources = this.$store.getters[`${ inStore }/all`](HCI.VMI);
|
||||||
|
const resource = vmiResources.find((VMI) => VMI.id === this.actionResource?.id) || null;
|
||||||
|
|
||||||
vmsByNode() {
|
return resource;
|
||||||
const groups = {};
|
|
||||||
|
|
||||||
for (const r of this.resources) {
|
|
||||||
const node = r.nodeName || '';
|
|
||||||
const name = r.nameDisplay || r.name || r.id;
|
|
||||||
|
|
||||||
if (!groups[node]) {
|
|
||||||
groups[node] = [];
|
|
||||||
}
|
|
||||||
groups[node].push(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.entries(groups).map(([node, vms]) => ({ node, vms })).sort((a, b) => a.node.localeCompare(b.node));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
cpuPinningAlertMessage() {
|
cpuPinningAlertMessage() {
|
||||||
return this.t('harvester.virtualMachine.cpuPinning.migrationMessage');
|
return this.t('harvester.virtualMachine.cpuPinning.migrationMessage');
|
||||||
},
|
},
|
||||||
|
|
||||||
allVmsOnTargetNode() {
|
|
||||||
if (!this.nodeName) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.resources.every((r) => r.nodeName === this.nodeName);
|
|
||||||
},
|
|
||||||
|
|
||||||
nodeNameList() {
|
nodeNameList() {
|
||||||
const nodes = this.$store.getters['harvester/all'](NODE);
|
const nodes = this.$store.getters['harvester/all'](NODE);
|
||||||
|
|
||||||
return nodes.filter((n) => {
|
return nodes.filter((n) => {
|
||||||
|
const isNotSelfNode = !!this.availableNodes.includes(n.id);
|
||||||
const isNotWitnessNode = n.isEtcd !== 'true'; // do not allow to migrate to self node and witness node
|
const isNotWitnessNode = n.isEtcd !== 'true'; // do not allow to migrate to self node and witness node
|
||||||
const matchingCpuManagerConfig = !this.anyCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
|
const isCpuPinning = this.actionResource?.isCpuPinning;
|
||||||
|
const matchingCpuManagerConfig = !isCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
|
||||||
|
|
||||||
return isNotWitnessNode && matchingCpuManagerConfig;
|
return isNotSelfNode && isNotWitnessNode && matchingCpuManagerConfig;
|
||||||
}).map((n) => {
|
}).map((n) => {
|
||||||
let label = n?.metadata?.name;
|
let label = n?.metadata?.name;
|
||||||
const value = n?.metadata?.name;
|
const value = n?.metadata?.name;
|
||||||
@ -141,29 +126,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Filter out VMs already running on the selected node
|
await this.actionResource.doAction('migrate', { nodeName: this.nodeName }, {}, false);
|
||||||
const toMigrate = this.resources.filter((r) => r.nodeName !== this.nodeName);
|
|
||||||
|
|
||||||
// await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
|
|
||||||
// We want to show all migration errors if there are multiple VMs, so we use allSettled here and handle the results accordingly.
|
|
||||||
const results = await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
|
|
||||||
|
|
||||||
const failedMigrations = results
|
|
||||||
.map((result, index) => ({ resource: toMigrate[index], result }))
|
|
||||||
.filter(({ result }) => result.status === 'rejected');
|
|
||||||
|
|
||||||
if (failedMigrations.length) {
|
|
||||||
this['errors'] = failedMigrations.flatMap(({ resource, result }) => {
|
|
||||||
const vmName = resource?.nameDisplay || resource?.name || resource?.metadata?.name || this.$store.getters['i18n/t']('generic.unknown');
|
|
||||||
const error = result.reason?.data || result.reason;
|
|
||||||
const messages = exceptionToErrorsArray(error);
|
|
||||||
|
|
||||||
return messages.map((message) => `${ vmName }: ${ message }`);
|
|
||||||
});
|
|
||||||
buttonDone(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
buttonDone(true);
|
buttonDone(true);
|
||||||
this.close();
|
this.close();
|
||||||
@ -183,35 +146,17 @@ export default {
|
|||||||
<template>
|
<template>
|
||||||
<Card :show-highlight-border="false">
|
<Card :show-highlight-border="false">
|
||||||
<template #title>
|
<template #title>
|
||||||
{{ t('harvester.modal.migration.vmMigrationTitle', { count: resources.length }) }}
|
{{ t('harvester.modal.migration.title') }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body>
|
<template #body>
|
||||||
<Banner
|
<Banner
|
||||||
v-if="anyCpuPinning"
|
v-if="actionResource?.isCpuPinning"
|
||||||
color="warning"
|
color="warning"
|
||||||
:label="cpuPinningAlertMessage"
|
:label="cpuPinningAlertMessage"
|
||||||
/>
|
/>
|
||||||
<p>
|
|
||||||
{{ t('harvester.modal.migration.selectedVMs') }}
|
|
||||||
</p>
|
|
||||||
<ul class="vm-list">
|
|
||||||
<li
|
|
||||||
v-for="group in vmsByNode"
|
|
||||||
:key="group.node"
|
|
||||||
>
|
|
||||||
{{ group.node || t('harvester.modal.migration.unknownNode') }}: {{ group.vms.join(', ') }}
|
|
||||||
<span
|
|
||||||
v-if="nodeName && group.node === nodeName"
|
|
||||||
class="already-on-target"
|
|
||||||
>
|
|
||||||
({{ t('harvester.modal.migration.alreadyOnTarget') }})
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="nodeName"
|
v-model:value="nodeName"
|
||||||
class="mt-15"
|
|
||||||
:label="t('harvester.modal.migration.fields.nodeName.label')"
|
:label="t('harvester.modal.migration.fields.nodeName.label')"
|
||||||
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
|
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
|
||||||
:options="nodeNameList"
|
:options="nodeNameList"
|
||||||
@ -238,7 +183,7 @@ export default {
|
|||||||
|
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
mode="apply"
|
mode="apply"
|
||||||
:disabled="!nodeName || allVmsOnTargetNode"
|
:disabled="!nodeName"
|
||||||
@click="apply"
|
@click="apply"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -256,16 +201,4 @@ export default {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.already-on-target {
|
|
||||||
color: var(--warning);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vm-list {
|
|
||||||
list-style: disc;
|
|
||||||
padding-left: 1.5em;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import FileSelector, { createOnSelected } from '@shell/components/form/FileSelec
|
|||||||
|
|
||||||
import { randomStr } from '@shell/utils/string';
|
import { randomStr } from '@shell/utils/string';
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
import { getLoginAwareErrors } from '../utils/error';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditKeypair',
|
name: 'HarvesterEditKeypair',
|
||||||
@ -64,14 +63,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
|
||||||
normalizedErrors() {
|
|
||||||
const message = this.t('harvester.virtualMachine.genericLoginError');
|
|
||||||
|
|
||||||
return getLoginAwareErrors(this.errors, message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: { onKeySelected: createOnSelected('publicKey') },
|
methods: { onKeySelected: createOnSelected('publicKey') },
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -81,9 +72,10 @@ export default {
|
|||||||
:done-route="doneRoute"
|
:done-route="doneRoute"
|
||||||
:resource="value"
|
:resource="value"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:errors="normalizedErrors"
|
:errors="errors"
|
||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
|
@error="e=>errors=e"
|
||||||
>
|
>
|
||||||
<div class="header mb-20">
|
<div class="header mb-20">
|
||||||
<FileSelector
|
<FileSelector
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import { _VIEW } from '@shell/config/query-params';
|
|||||||
|
|
||||||
import { NAMESPACE } from '@shell/config/types';
|
import { NAMESPACE } from '@shell/config/types';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
import { getLoginAwareErrors } from '../../utils/error';
|
|
||||||
|
|
||||||
const _NEW = '_NEW';
|
const _NEW = '_NEW';
|
||||||
|
|
||||||
@ -215,9 +214,7 @@ export default {
|
|||||||
buttonCb(true);
|
buttonCb(true);
|
||||||
this.cancel();
|
this.cancel();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = this.t('harvester.virtualMachine.genericLoginError');
|
this.errors = [err.message];
|
||||||
|
|
||||||
this.errors = getLoginAwareErrors(err, message);
|
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -64,12 +64,6 @@ export default {
|
|||||||
value: 'status.productID',
|
value: 'status.productID',
|
||||||
sort: ['status.productID', 'status.vendorID']
|
sort: ['status.productID', 'status.vendorID']
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'classType',
|
|
||||||
labelKey: 'harvester.usb.classType',
|
|
||||||
value: 'status.classType',
|
|
||||||
sort: ['status.classType']
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!isSingleProduct) {
|
if (!isSingleProduct) {
|
||||||
|
|||||||
@ -144,10 +144,6 @@ harvester:
|
|||||||
migration:
|
migration:
|
||||||
failedMessage: Latest migration failed!
|
failedMessage: Latest migration failed!
|
||||||
title: Migration
|
title: Migration
|
||||||
vmMigrationTitle: '{count, plural, one {Migrating # VM} other {Migrating # VMs}}'
|
|
||||||
selectedVMs: "The following virtual machine(s) will be migrated to the target node"
|
|
||||||
unknownNode: (unknown node)
|
|
||||||
alreadyOnTarget: Already on Target
|
|
||||||
fields:
|
fields:
|
||||||
nodeName:
|
nodeName:
|
||||||
label: Target Node
|
label: Target Node
|
||||||
@ -252,7 +248,6 @@ harvester:
|
|||||||
suspendSchedule: Suspend
|
suspendSchedule: Suspend
|
||||||
restoreExistingVM: Replace Existing
|
restoreExistingVM: Replace Existing
|
||||||
migrate: Migrate
|
migrate: Migrate
|
||||||
vmMigrate: Virtual Machine Migration
|
|
||||||
cpuAndMemoryHotplug: Edit CPU and Memory
|
cpuAndMemoryHotplug: Edit CPU and Memory
|
||||||
abortMigration: Abort Migration
|
abortMigration: Abort Migration
|
||||||
storageMigration: Storage Migration
|
storageMigration: Storage Migration
|
||||||
@ -303,7 +298,6 @@ harvester:
|
|||||||
phase: Phase
|
phase: Phase
|
||||||
attachedVM: Attached Virtual Machine
|
attachedVM: Attached Virtual Machine
|
||||||
cpuManager: CPU Manager
|
cpuManager: CPU Manager
|
||||||
routeConnectivityTooltip: Connectivity between the VM network and the management network, which the Harvester nodes are connected to.
|
|
||||||
fingerprint: Fingerprint
|
fingerprint: Fingerprint
|
||||||
value: Value
|
value: Value
|
||||||
actions: Actions
|
actions: Actions
|
||||||
@ -732,7 +726,6 @@ harvester:
|
|||||||
other {Start}
|
other {Start}
|
||||||
} Now
|
} Now
|
||||||
createSSHKey: Create a New...
|
createSSHKey: Create a New...
|
||||||
genericLoginError: Authentication failed. Please re-log in and try again.
|
|
||||||
installAgent: Install guest agent
|
installAgent: Install guest agent
|
||||||
enableUsb: Enable USB Tablet
|
enableUsb: Enable USB Tablet
|
||||||
advancedOptions:
|
advancedOptions:
|
||||||
@ -1292,13 +1285,6 @@ harvester:
|
|||||||
rancherCluster:
|
rancherCluster:
|
||||||
kubeConfig: Rancher KubeConfig
|
kubeConfig: Rancher KubeConfig
|
||||||
removeUpstreamClusterWhenNamespaceIsDeleted: Remove Upstream Cluster When Namespace Is Deleted
|
removeUpstreamClusterWhenNamespaceIsDeleted: Remove Upstream Cluster When Namespace Is Deleted
|
||||||
clusterPodSecurityStandard:
|
|
||||||
whitelistedNamespaces:
|
|
||||||
label: 'Whitelisted Namespaces'
|
|
||||||
privilegedNamespaces:
|
|
||||||
label: 'Privileged Namespaces'
|
|
||||||
restrictedNamespaces:
|
|
||||||
label: 'Restricted Namespaces'
|
|
||||||
storageNetwork:
|
storageNetwork:
|
||||||
range:
|
range:
|
||||||
placeholder: e.g. 172.16.0.0/24
|
placeholder: e.g. 172.16.0.0/24
|
||||||
@ -1778,8 +1764,6 @@ harvester:
|
|||||||
repository: Image Repository
|
repository: Image Repository
|
||||||
driver:
|
driver:
|
||||||
location: Driver Location
|
location: Driver Location
|
||||||
enable:
|
|
||||||
title: Enable NVIDIA Driver Toolkit
|
|
||||||
parsingSpecError:
|
parsingSpecError:
|
||||||
The field 'spec.valuesContent' has invalid format.
|
The field 'spec.valuesContent' has invalid format.
|
||||||
usbController:
|
usbController:
|
||||||
@ -1971,7 +1955,6 @@ harvester:
|
|||||||
title: Cannot Disable Passthrough
|
title: Cannot Disable Passthrough
|
||||||
message: Please detach the device from the VM and save it first before disabling passthrough.
|
message: Please detach the device from the VM and save it first before disabling passthrough.
|
||||||
enablePassthroughWarning: 'Please re-enable the USB device if the device path changes in the following situations:<br/> 1) Re-plugging the USB device.<br/> 2) Rebooting the node.<br/><br/>An incorrect device path may cause passthrough to fail.'
|
enablePassthroughWarning: 'Please re-enable the USB device if the device path changes in the following situations:<br/> 1) Re-plugging the USB device.<br/> 2) Rebooting the node.<br/><br/>An incorrect device path may cause passthrough to fail.'
|
||||||
classType: Class Type
|
|
||||||
|
|
||||||
harvesterVlanConfigMigrateDialog:
|
harvesterVlanConfigMigrateDialog:
|
||||||
targetClusterNetwork:
|
targetClusterNetwork:
|
||||||
@ -2056,7 +2039,6 @@ advancedSettings:
|
|||||||
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
|
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
|
||||||
'harv-kubevirt-migration': 'Configure cluster-wide KubeVirt live migration parameters.'
|
'harv-kubevirt-migration': 'Configure cluster-wide KubeVirt live migration parameters.'
|
||||||
'harv-instance-manager-resources': 'Configure resource percentage reservations for Longhorn instance manager V1 and V2. Valid instance manager CPU range between 0 - 40.'
|
'harv-instance-manager-resources': 'Configure resource percentage reservations for Longhorn instance manager V1 and V2. Valid instance manager CPU range between 0 - 40.'
|
||||||
'harv-cluster-pod-security-standard': 'Enforce Kubernetes Pod Security Standards (PSS) at the cluster level.'
|
|
||||||
|
|
||||||
typeLabel:
|
typeLabel:
|
||||||
kubevirt.io.virtualmachine: |-
|
kubevirt.io.virtualmachine: |-
|
||||||
@ -2179,7 +2161,11 @@ typeLabel:
|
|||||||
one { PCI Device }
|
one { PCI Device }
|
||||||
other { PCI Devices }
|
other { PCI Devices }
|
||||||
}
|
}
|
||||||
|
persistentvolumeclaim: |-
|
||||||
|
{count, plural,
|
||||||
|
one { Volume }
|
||||||
|
other { Volumes }
|
||||||
|
}
|
||||||
network.harvesterhci.io.clusternetwork: |-
|
network.harvesterhci.io.clusternetwork: |-
|
||||||
{count, plural,
|
{count, plural,
|
||||||
one { Cluster Network }
|
one { Cluster Network }
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Banner } from '@components/Banner';
|
|||||||
import Loading from '@shell/components/Loading';
|
import Loading from '@shell/components/Loading';
|
||||||
import ResourceTable from '@shell/components/ResourceTable';
|
import ResourceTable from '@shell/components/ResourceTable';
|
||||||
import BadgeState from '@shell/components/formatter/BadgeStateFormatter';
|
import BadgeState from '@shell/components/formatter/BadgeStateFormatter';
|
||||||
|
|
||||||
import { NAME, AGE, NAMESPACE, STATE } from '@shell/config/table-headers';
|
import { NAME, AGE, NAMESPACE, STATE } from '@shell/config/table-headers';
|
||||||
import { NETWORK_ATTACHMENT, SCHEMA } from '@shell/config/types';
|
import { NETWORK_ATTACHMENT, SCHEMA } from '@shell/config/types';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
@ -112,7 +113,6 @@ export default {
|
|||||||
value: 'connectivity',
|
value: 'connectivity',
|
||||||
labelKey: 'tableHeaders.routeConnectivity',
|
labelKey: 'tableHeaders.routeConnectivity',
|
||||||
formatter: 'NetworkRouteConnectivity',
|
formatter: 'NetworkRouteConnectivity',
|
||||||
tooltip: 'harvester.tableHeaders.routeConnectivityTooltip',
|
|
||||||
formatterOpts: { arbitrary: true },
|
formatterOpts: { arbitrary: true },
|
||||||
width: 130,
|
width: 130,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -12,11 +12,6 @@ import { HCI } from '../types';
|
|||||||
import HarvesterVmState from '../formatters/HarvesterVmState';
|
import HarvesterVmState from '../formatters/HarvesterVmState';
|
||||||
import ConsoleBar from '../components/VMConsoleBar';
|
import ConsoleBar from '../components/VMConsoleBar';
|
||||||
|
|
||||||
const ENCRYPTED_VOLUME_TOOLTIP_KEYS = {
|
|
||||||
all: 'harvester.virtualMachine.volume.lockTooltip.all',
|
|
||||||
partial: 'harvester.virtualMachine.volume.lockTooltip.partial',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const VM_HEADERS = [
|
export const VM_HEADERS = [
|
||||||
STATE,
|
STATE,
|
||||||
{
|
{
|
||||||
@ -168,12 +163,6 @@ export default {
|
|||||||
*/
|
*/
|
||||||
hasBackUpRestoreInProgress() {
|
hasBackUpRestoreInProgress() {
|
||||||
return !!this.rows.find((r) => r.restoreResource && !r.restoreResource.fromSnapshot && !r.restoreResource.isComplete);
|
return !!this.rows.find((r) => r.restoreResource && !r.restoreResource.fromSnapshot && !r.restoreResource.isComplete);
|
||||||
},
|
|
||||||
|
|
||||||
vmRestartRequiredNames() {
|
|
||||||
return this.allVMs
|
|
||||||
.filter((vm) => vm.isRestartRequired)
|
|
||||||
.map((vm) => vm.metadata.name);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -192,35 +181,53 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
vmRestartRequiredNames(vmNames) {
|
allVMs: {
|
||||||
const count = vmNames.length;
|
handler(neu) {
|
||||||
|
const vmNames = [];
|
||||||
|
|
||||||
if (count === 0 && this.restartNotificationDisplayed) {
|
neu.forEach((vm) => {
|
||||||
this.restartNotificationDisplayed = false;
|
if (vm.isRestartRequired) {
|
||||||
|
vmNames.push(vm.metadata.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const count = vmNames.length;
|
||||||
|
|
||||||
return;
|
if ( count === 0 && this.restartNotificationDisplayed) {
|
||||||
}
|
this.restartNotificationDisplayed = false;
|
||||||
|
|
||||||
if (count > 0) {
|
return;
|
||||||
// clear old notification before showing new one
|
|
||||||
if (this.restartNotificationDisplayed) {
|
|
||||||
this.$store.dispatch('growl/clear');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$store.dispatch('growl/warning', {
|
if (count > 0) {
|
||||||
title: this.t('harvester.notification.restartRequired.title', { count }),
|
// clear old notification before showing new one
|
||||||
message: this.t('harvester.notification.restartRequired.message', { vmNames: vmNames.join(', ') }),
|
if (this.restartNotificationDisplayed) {
|
||||||
timeout: 10000,
|
this.$store.dispatch('growl/clear');
|
||||||
}, { root: true });
|
}
|
||||||
this.restartNotificationDisplayed = true;
|
}
|
||||||
}
|
|
||||||
|
if (count > 0 && vmNames.length > 0) {
|
||||||
|
this.$store.dispatch('growl/warning', {
|
||||||
|
title: this.t('harvester.notification.restartRequired.title', { count }),
|
||||||
|
message: this.t('harvester.notification.restartRequired.message', { vmNames: vmNames.join(', ') }),
|
||||||
|
timeout: 10000,
|
||||||
|
}, { root: true });
|
||||||
|
this.restartNotificationDisplayed = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
lockIconTooltipMessage(row) {
|
lockIconTooltipMessage(row) {
|
||||||
const key = ENCRYPTED_VOLUME_TOOLTIP_KEYS[row.encryptedVolumeType];
|
const message = '';
|
||||||
|
|
||||||
return key ? this.t(key) : '';
|
if (row.encryptedVolumeType === 'all') {
|
||||||
|
return this.t('harvester.virtualMachine.volume.lockTooltip.all');
|
||||||
|
} else if (row.encryptedVolumeType === 'partial') {
|
||||||
|
return this.t('harvester.virtualMachine.volume.lockTooltip.partial');
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -260,7 +267,7 @@ export default {
|
|||||||
>
|
>
|
||||||
{{ scope.row.metadata.name }}
|
{{ scope.row.metadata.name }}
|
||||||
<i
|
<i
|
||||||
v-if="scope.row.encryptedVolumeType !== 'none'"
|
v-if="lockIconTooltipMessage(scope.row)"
|
||||||
v-tooltip="lockIconTooltipMessage(scope.row)"
|
v-tooltip="lockIconTooltipMessage(scope.row)"
|
||||||
class="icon icon-lock"
|
class="icon icon-lock"
|
||||||
:class="{'green-icon': scope.row.encryptedVolumeType === 'all', 'yellow-icon': scope.row.encryptedVolumeType === 'partial'}"
|
:class="{'green-icon': scope.row.encryptedVolumeType === 'all', 'yellow-icon': scope.row.encryptedVolumeType === 'partial'}"
|
||||||
|
|||||||
@ -30,12 +30,11 @@ const STATUS_DISPLAY = {
|
|||||||
export default class PCIDevice extends SteveModel {
|
export default class PCIDevice extends SteveModel {
|
||||||
get _availableActions() {
|
get _availableActions() {
|
||||||
const out = super._availableActions;
|
const out = super._availableActions;
|
||||||
const canUpdate = !!this.linkFor('update');
|
|
||||||
|
|
||||||
out.push(
|
out.push(
|
||||||
{
|
{
|
||||||
action: 'enablePassthroughBulk',
|
action: 'enablePassthroughBulk',
|
||||||
enabled: !this.isEnabling && !this.isvGPUDevice && canUpdate,
|
enabled: !this.isEnabling && !this.isvGPUDevice,
|
||||||
icon: 'icon icon-fw icon-dot',
|
icon: 'icon icon-fw icon-dot',
|
||||||
label: 'Enable Passthrough',
|
label: 'Enable Passthrough',
|
||||||
bulkable: true,
|
bulkable: true,
|
||||||
@ -44,7 +43,7 @@ export default class PCIDevice extends SteveModel {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'disablePassthrough',
|
action: 'disablePassthrough',
|
||||||
enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice && canUpdate,
|
enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice,
|
||||||
icon: 'icon icon-fw icon-dot-open',
|
icon: 'icon icon-fw icon-dot-open',
|
||||||
label: 'Disable Passthrough',
|
label: 'Disable Passthrough',
|
||||||
bulkable: true,
|
bulkable: true,
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const OBSCURE_NAMESPACE_PREFIX = [
|
|||||||
|
|
||||||
export default class HciNamespace extends namespace {
|
export default class HciNamespace extends namespace {
|
||||||
get _availableActions() {
|
get _availableActions() {
|
||||||
let out = super._availableActions;
|
const out = super._availableActions;
|
||||||
const remove = out.findIndex((a) => a.action === 'promptRemove');
|
const remove = out.findIndex((a) => a.action === 'promptRemove');
|
||||||
|
|
||||||
const promptRemove = {
|
const promptRemove = {
|
||||||
@ -53,16 +53,6 @@ export default class HciNamespace extends namespace {
|
|||||||
insertAt(out, out.length - 1, promptRemove);
|
insertAt(out, out.length - 1, promptRemove);
|
||||||
insertAt(out, out.length - 5, editQuotaAction);
|
insertAt(out, out.length - 5, editQuotaAction);
|
||||||
|
|
||||||
const canUpdate = !!this.linkFor('update');
|
|
||||||
|
|
||||||
out = out.map((action) => {
|
|
||||||
if (['move'].includes(action.action)) {
|
|
||||||
return { ...action, enabled: action.enabled && canUpdate };
|
|
||||||
}
|
|
||||||
|
|
||||||
return action;
|
|
||||||
});
|
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,20 +5,6 @@ import Secret from '@shell/models/secret';
|
|||||||
import { NAMESPACE } from '@shell/config/types';
|
import { NAMESPACE } from '@shell/config/types';
|
||||||
|
|
||||||
export default class HciSecret extends Secret {
|
export default class HciSecret extends Secret {
|
||||||
get _availableActions() {
|
|
||||||
let out = super._availableActions;
|
|
||||||
|
|
||||||
out = out.map((action) => {
|
|
||||||
if (['download'].includes(action.action)) {
|
|
||||||
return { ...action, enabled: !!this.linkFor('update') };
|
|
||||||
}
|
|
||||||
|
|
||||||
return action;
|
|
||||||
});
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// prevent harvester secret detail page be overridden.
|
// prevent harvester secret detail page be overridden.
|
||||||
// See isFullPageOverride in https://github.com/rancher/dashboard/blob/master/shell/components/ResourceDetail/index.vue
|
// See isFullPageOverride in https://github.com/rancher/dashboard/blob/master/shell/components/ResourceDetail/index.vue
|
||||||
get fullDetailPageOverride() {
|
get fullDetailPageOverride() {
|
||||||
|
|||||||
@ -106,15 +106,6 @@ export default class HciStorageClass extends StorageClass {
|
|||||||
|
|
||||||
get availableActions() {
|
get availableActions() {
|
||||||
let out = super.availableActions || [];
|
let out = super.availableActions || [];
|
||||||
const canUpdate = !!this.linkFor('update');
|
|
||||||
|
|
||||||
out = out.map((action) => {
|
|
||||||
if (['setDefault', 'setAsDefault', 'resetDefault'].includes(action.action)) {
|
|
||||||
return { ...action, enabled: canUpdate };
|
|
||||||
}
|
|
||||||
|
|
||||||
return action;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.isInternalStorageClass()) {
|
if (this.isInternalStorageClass()) {
|
||||||
out = out.filter((action) => {
|
out = out.filter((action) => {
|
||||||
|
|||||||
@ -4,8 +4,6 @@ import { HCI as HCI_ANNOTATIONS } from '../config/labels-annotations';
|
|||||||
import HarvesterResource from './harvester';
|
import HarvesterResource from './harvester';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
|
|
||||||
const HARVESTER_NVIDIA_DRIVER_TOOLKIT = 'harvester-system/nvidia-driver-toolkit';
|
|
||||||
|
|
||||||
export default class HciAddonConfig extends HarvesterResource {
|
export default class HciAddonConfig extends HarvesterResource {
|
||||||
get availableActions() {
|
get availableActions() {
|
||||||
const out = super._availableActions;
|
const out = super._availableActions;
|
||||||
@ -21,10 +19,9 @@ export default class HciAddonConfig extends HarvesterResource {
|
|||||||
out.push(rancherDashboard);
|
out.push(rancherDashboard);
|
||||||
}
|
}
|
||||||
|
|
||||||
const canUpdate = !!this.linkFor('update');
|
|
||||||
const toggleAddon = {
|
const toggleAddon = {
|
||||||
action: 'toggleAddon',
|
action: 'toggleAddon',
|
||||||
enabled: canUpdate,
|
enabled: true,
|
||||||
icon: this.spec.enabled ? 'icon icon-pause' : 'icon icon-play',
|
icon: this.spec.enabled ? 'icon icon-pause' : 'icon icon-play',
|
||||||
label: this.spec.enabled ? this.t('generic.disable') : this.t('generic.enable'),
|
label: this.spec.enabled ? this.t('generic.disable') : this.t('generic.enable'),
|
||||||
};
|
};
|
||||||
@ -48,15 +45,6 @@ export default class HciAddonConfig extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.spec.enabled && this.id === HARVESTER_NVIDIA_DRIVER_TOOLKIT) {
|
|
||||||
this.$dispatch('promptModal', {
|
|
||||||
resources: [this],
|
|
||||||
component: 'HarvesterEnableNvidiaDriverToolkit',
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.spec.enabled = !this.spec.enabled;
|
this.spec.enabled = !this.spec.enabled;
|
||||||
await this.save();
|
await this.save();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -3,20 +3,6 @@ import { findBy } from '@shell/utils/array';
|
|||||||
import HarvesterResource from './harvester';
|
import HarvesterResource from './harvester';
|
||||||
|
|
||||||
export default class HciKeypair extends HarvesterResource {
|
export default class HciKeypair extends HarvesterResource {
|
||||||
get _availableActions() {
|
|
||||||
let out = super._availableActions;
|
|
||||||
|
|
||||||
out = out.map((action) => {
|
|
||||||
if (['download'].includes(action.action)) {
|
|
||||||
return { ...action, enabled: !!this.linkFor('update') };
|
|
||||||
}
|
|
||||||
|
|
||||||
return action;
|
|
||||||
});
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
get stateDisplay() {
|
get stateDisplay() {
|
||||||
const conditions = get(this, 'status.conditions');
|
const conditions = get(this, 'status.conditions');
|
||||||
const status = (findBy(conditions, 'type', 'validated') || {}).status ;
|
const status = (findBy(conditions, 'type', 'validated') || {}).status ;
|
||||||
|
|||||||
@ -19,18 +19,16 @@ export default class ScheduleVmBackup extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const canUpdate = !!this.linkFor('update');
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
action: 'resumeSchedule',
|
action: 'resumeSchedule',
|
||||||
enabled: canUpdate && ucFirst(this.state) === STATES.suspended.label,
|
enabled: ucFirst(this.state) === STATES.suspended.label,
|
||||||
icon: 'icons icon-play',
|
icon: 'icons icon-play',
|
||||||
label: this.t('harvester.action.resumeSchedule'),
|
label: this.t('harvester.action.resumeSchedule'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'suspendSchedule',
|
action: 'suspendSchedule',
|
||||||
enabled: canUpdate && ucFirst(this.state) === STATES.active.label,
|
enabled: ucFirst(this.state) === STATES.active.label,
|
||||||
icon: 'icons icon-pause',
|
icon: 'icons icon-pause',
|
||||||
label: this.t('harvester.action.suspendSchedule'),
|
label: this.t('harvester.action.suspendSchedule'),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -39,6 +39,9 @@ function isReady() {
|
|||||||
export default class HciVmImage extends HarvesterResource {
|
export default class HciVmImage extends HarvesterResource {
|
||||||
get availableActions() {
|
get availableActions() {
|
||||||
let out = super._availableActions;
|
let out = super._availableActions;
|
||||||
|
const toFilter = ['goToEditYaml'];
|
||||||
|
|
||||||
|
out = out.filter( (A) => !toFilter.includes(A.action));
|
||||||
|
|
||||||
// show `Clone` only when imageSource is `download`
|
// show `Clone` only when imageSource is `download`
|
||||||
if (this.imageSource !== 'download') {
|
if (this.imageSource !== 'download') {
|
||||||
@ -52,7 +55,6 @@ export default class HciVmImage extends HarvesterResource {
|
|||||||
canCreateVM = false;
|
canCreateVM = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const canCreateImage = !!this.$getters?.['schemaFor']?.(HCI.IMAGE)?.collectionMethods?.some((method) => method.toLowerCase() === 'post');
|
|
||||||
const customActions = this.isReady ? [
|
const customActions = this.isReady ? [
|
||||||
{
|
{
|
||||||
action: 'createFromImage',
|
action: 'createFromImage',
|
||||||
@ -62,13 +64,13 @@ export default class HciVmImage extends HarvesterResource {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'encryptImage',
|
action: 'encryptImage',
|
||||||
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted && canCreateImage,
|
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted,
|
||||||
icon: 'icon icon-lock',
|
icon: 'icon icon-lock',
|
||||||
label: this.t('harvester.action.encryptImage'),
|
label: this.t('harvester.action.encryptImage'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'decryptImage',
|
action: 'decryptImage',
|
||||||
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted && canCreateImage,
|
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted,
|
||||||
icon: 'icon icon-unlock',
|
icon: 'icon icon-unlock',
|
||||||
label: this.t('harvester.action.decryptImage'),
|
label: this.t('harvester.action.decryptImage'),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -83,42 +83,6 @@ const VMIPhase = {
|
|||||||
|
|
||||||
let productInStore;
|
let productInStore;
|
||||||
|
|
||||||
let _podOwnerMap = null;
|
|
||||||
let _podOwnerMapSource = null;
|
|
||||||
|
|
||||||
function getPodByOwnerName(rootGetters, inStore, ownerName) {
|
|
||||||
const podList = rootGetters[`${ inStore }/all`](POD);
|
|
||||||
|
|
||||||
if (!Array.isArray(podList)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
// if not equals (usually means the pod list has been updated), we need to rebuild the map, otherwise we can reuse the map for better performance
|
|
||||||
if (_podOwnerMapSource !== podList) {
|
|
||||||
_podOwnerMap = new Map(); // use Map to store ownerReference name and pod mapping
|
|
||||||
for (const pod of podList) {
|
|
||||||
const refName = pod.metadata?.ownerReferences?.[0]?.name;
|
|
||||||
|
|
||||||
if (refName) {
|
|
||||||
_podOwnerMap.set(refName, pod);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_podOwnerMapSource = podList;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _podOwnerMap.get(ownerName);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPvcsByNames(rootGetters, inStore, names) {
|
|
||||||
const pvcList = rootGetters[`${ inStore }/all`](PVC);
|
|
||||||
|
|
||||||
if (!Array.isArray(pvcList)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const uniqueNames = new Set(names);
|
|
||||||
|
|
||||||
return pvcList.filter((pvc) => uniqueNames.has(pvc.metadata?.name));
|
|
||||||
}
|
|
||||||
|
|
||||||
const IgnoreMessages = ['pod has unbound immediate PersistentVolumeClaims'];
|
const IgnoreMessages = ['pod has unbound immediate PersistentVolumeClaims'];
|
||||||
|
|
||||||
export default class VirtVm extends HarvesterResource {
|
export default class VirtVm extends HarvesterResource {
|
||||||
@ -130,8 +94,6 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
clone.action = 'goToCloneVM';
|
clone.action = 'goToCloneVM';
|
||||||
}
|
}
|
||||||
|
|
||||||
const canCreateVMSSchedule = !!this.$getters?.['schemaFor']?.(HCI.SCHEDULE_VM_BACKUP)?.collectionMethods?.find((x) => ['post'].includes(x.toLowerCase()));
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
action: 'stopVM',
|
action: 'stopVM',
|
||||||
@ -164,7 +126,6 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'restartVM',
|
action: 'restartVM',
|
||||||
altAction: 'altRestartVM',
|
|
||||||
enabled: !!this.actions?.restart,
|
enabled: !!this.actions?.restart,
|
||||||
icon: 'icon icon-refresh',
|
icon: 'icon icon-refresh',
|
||||||
label: this.t('harvester.action.restart'),
|
label: this.t('harvester.action.restart'),
|
||||||
@ -172,11 +133,10 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
bulkAction: 'restartVM'
|
bulkAction: 'restartVM'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'softrebootVM',
|
action: 'softrebootVM',
|
||||||
altAction: 'doSoftReboot',
|
enabled: !!this.actions?.softreboot,
|
||||||
enabled: !!this.actions?.softreboot,
|
icon: 'icon icon-pipeline',
|
||||||
icon: 'icon icon-pipeline',
|
label: this.t('harvester.action.softreboot')
|
||||||
label: this.t('harvester.action.softreboot')
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'startVM',
|
action: 'startVM',
|
||||||
@ -211,7 +171,7 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'createSchedule',
|
action: 'createSchedule',
|
||||||
enabled: canCreateVMSSchedule && this.schedulingVMBackupFeatureEnabled,
|
enabled: this.schedulingVMBackupFeatureEnabled,
|
||||||
icon: 'icon icon-history',
|
icon: 'icon icon-history',
|
||||||
label: this.t('harvester.action.createSchedule')
|
label: this.t('harvester.action.createSchedule')
|
||||||
},
|
},
|
||||||
@ -228,12 +188,10 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
label: this.t('harvester.action.ejectCDROM')
|
label: this.t('harvester.action.ejectCDROM')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'migrateVM',
|
action: 'migrateVM',
|
||||||
enabled: !!this.actions?.migrate,
|
enabled: !!this.actions?.migrate,
|
||||||
icon: 'icon icon-copy',
|
icon: 'icon icon-copy',
|
||||||
label: this.t('harvester.action.vmMigrate'),
|
label: this.t('harvester.action.migrate')
|
||||||
bulkable: true,
|
|
||||||
bulkAction: 'migrateVM'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'abortMigrationVM',
|
action: 'abortMigrationVM',
|
||||||
@ -373,10 +331,6 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
this.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
this.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
altRestartVM() {
|
|
||||||
this.doActionGrowl('restart', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
restartVM(resources = this) {
|
restartVM(resources = this) {
|
||||||
this.$dispatch('promptModal', {
|
this.$dispatch('promptModal', {
|
||||||
resources,
|
resources,
|
||||||
@ -706,13 +660,16 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
|
|
||||||
get podResource() {
|
get podResource() {
|
||||||
const inStore = this.productInStore;
|
const inStore = this.productInStore;
|
||||||
|
|
||||||
const vmiResource = this.$rootGetters[`${ inStore }/byId`](HCI.VMI, this.id);
|
const vmiResource = this.$rootGetters[`${ inStore }/byId`](HCI.VMI, this.id);
|
||||||
|
const podList = this.$rootGetters[`${ inStore }/all`](POD);
|
||||||
|
|
||||||
if (!vmiResource?.metadata?.name) {
|
return podList.find((P) => {
|
||||||
return undefined;
|
return (
|
||||||
}
|
vmiResource?.metadata?.name &&
|
||||||
|
vmiResource?.metadata?.name === P.metadata?.ownerReferences?.[0].name
|
||||||
return getPodByOwnerName(this.$rootGetters, inStore, vmiResource.metadata.name);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get isPaused() {
|
get isPaused() {
|
||||||
@ -753,13 +710,17 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
get vmi() {
|
get vmi() {
|
||||||
const inStore = this.productInStore;
|
const inStore = this.productInStore;
|
||||||
|
|
||||||
return this.$rootGetters[`${ inStore }/byId`](HCI.VMI, this.id);
|
const vmis = this.$rootGetters[`${ inStore }/all`](HCI.VMI);
|
||||||
|
|
||||||
|
return vmis.find((VMI) => VMI.id === this.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
get volumes() {
|
get volumes() {
|
||||||
|
const pvcs = this.$rootGetters[`${ this.productInStore }/all`](PVC);
|
||||||
|
|
||||||
const volumeClaimNames = this.spec.template.spec.volumes?.map((v) => v.persistentVolumeClaim?.claimName).filter((v) => !!v) || [];
|
const volumeClaimNames = this.spec.template.spec.volumes?.map((v) => v.persistentVolumeClaim?.claimName).filter((v) => !!v) || [];
|
||||||
|
|
||||||
return getPvcsByNames(this.$rootGetters, this.productInStore, volumeClaimNames);
|
return pvcs.filter((pvc) => volumeClaimNames.includes(pvc.metadata.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
get lvmVolumes() {
|
get lvmVolumes() {
|
||||||
@ -792,6 +753,17 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
return { status: 'VMI error', detailedMessage: vmiFailureCond.message };
|
return { status: 'VMI error', detailedMessage: vmiFailureCond.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((this.vmi || this.isVMCreated) && this.podResource) {
|
||||||
|
// const podStatus = this.podResource.getPodStatus;
|
||||||
|
// if (POD_STATUS_ALL_ERROR.includes(podStatus?.status)) {
|
||||||
|
// return {
|
||||||
|
// ...podStatus,
|
||||||
|
// status: 'LAUNCHER_POD_ERROR',
|
||||||
|
// pod: this.podResource,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
return this?.vmi?.status?.phase;
|
return this?.vmi?.status?.phase;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -929,7 +901,9 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
|
|
||||||
const inStore = this.productInStore;
|
const inStore = this.productInStore;
|
||||||
|
|
||||||
const res = this.$rootGetters[`${ inStore }/byId`](HCI.RESTORE, id);
|
const allRestore = this.$rootGetters[`${ inStore }/all`](HCI.RESTORE);
|
||||||
|
|
||||||
|
const res = allRestore.find((O) => O.id === id);
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
const allBackups = this.$rootGetters[`${ inStore }/all`](HCI.BACKUP);
|
const allBackups = this.$rootGetters[`${ inStore }/all`](HCI.BACKUP);
|
||||||
@ -1099,6 +1073,42 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get warningCount() {
|
||||||
|
return this.resourcesStatus.warningCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
get errorCount() {
|
||||||
|
return this.resourcesStatus.errorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
get resourcesStatus() {
|
||||||
|
const inStore = this.productInStore;
|
||||||
|
const vmList = this.$rootGetters[`${ inStore }/all`](HCI.VM);
|
||||||
|
let warningCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
vmList.forEach((vm) => {
|
||||||
|
const status = vm.actualState;
|
||||||
|
|
||||||
|
if (status === VM_ERROR) {
|
||||||
|
errorCount += 1;
|
||||||
|
} else if (
|
||||||
|
status === 'Stopping' ||
|
||||||
|
status === 'Waiting' ||
|
||||||
|
status === 'Pending' ||
|
||||||
|
status === 'Starting' ||
|
||||||
|
status === 'Terminating'
|
||||||
|
) {
|
||||||
|
warningCount += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
warningCount,
|
||||||
|
errorCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
get volumeClaimTemplates() {
|
get volumeClaimTemplates() {
|
||||||
return parseVolumeClaimTemplates(this);
|
return parseVolumeClaimTemplates(this);
|
||||||
}
|
}
|
||||||
@ -1116,6 +1126,7 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
get rootImageId() {
|
get rootImageId() {
|
||||||
let imageId = '';
|
let imageId = '';
|
||||||
const inStore = this.productInStore;
|
const inStore = this.productInStore;
|
||||||
|
const pvcs = this.$rootGetters[`${ inStore }/all`](PVC) || [];
|
||||||
|
|
||||||
const volumes = this.spec.template.spec.volumes || [];
|
const volumes = this.spec.template.spec.volumes || [];
|
||||||
|
|
||||||
@ -1125,7 +1136,9 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!isNoExistingVolume) {
|
if (!isNoExistingVolume) {
|
||||||
const existingVolume = this.$rootGetters[`${ inStore }/byId`](PVC, `${ this.metadata.namespace }/${ firstVolumeName }`);
|
const existingVolume = pvcs.find(
|
||||||
|
(P) => P.id === `${ this.metadata.namespace }/${ firstVolumeName }`
|
||||||
|
);
|
||||||
|
|
||||||
if (existingVolume) {
|
if (existingVolume) {
|
||||||
return existingVolume?.metadata?.annotations?.[
|
return existingVolume?.metadata?.annotations?.[
|
||||||
@ -1303,7 +1316,8 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isBackupTargetUnavailable() {
|
get isBackupTargetUnavailable() {
|
||||||
const backupTargetSetting = this.$rootGetters['harvester/byId'](HCI.SETTING, 'backup-target');
|
const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || [];
|
||||||
|
const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target');
|
||||||
|
|
||||||
return isBackupTargetSettingUnavailable(backupTargetSetting);
|
return isBackupTargetSettingUnavailable(backupTargetSetting);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
|
|
||||||
import shellProject from '@shell/models/management.cattle.io.project';
|
|
||||||
|
|
||||||
// This model controls `Project / Namespace` page in rancher integration mode
|
|
||||||
// Extend management.cattle.io.project model from shell
|
|
||||||
export default class Project extends shellProject {
|
|
||||||
get _availableActions() {
|
|
||||||
const canUpdate = !!this.linkFor('update');
|
|
||||||
|
|
||||||
// disable `Edit Config` action if user does not have update permission.
|
|
||||||
return super._availableActions.map((action) => {
|
|
||||||
if (action.action === 'goToEdit') {
|
|
||||||
return { ...action, enabled: canUpdate };
|
|
||||||
}
|
|
||||||
|
|
||||||
return action;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -57,11 +57,7 @@ export default class HciVlanConfig extends HarvesterResource {
|
|||||||
get _availableActions() {
|
get _availableActions() {
|
||||||
const out = super._availableActions;
|
const out = super._availableActions;
|
||||||
|
|
||||||
const canMigrate = !!this.$getters?.['schemaFor']?.(HCI.VLAN_CONFIG)?.collectionMethods?.find((x) => ['post'].includes(x.toLowerCase()));
|
insertAt(out, 0, this.migrateAction);
|
||||||
|
|
||||||
if (canMigrate) {
|
|
||||||
insertAt(out, 0, this.migrateAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester",
|
"name": "harvester",
|
||||||
"description": "Rancher UI Extension for Harvester",
|
"description": "Rancher UI Extension for Harvester",
|
||||||
"version": "1.9.0-dev",
|
"version": "1.8.1-dev",
|
||||||
"private": false,
|
"private": false,
|
||||||
"rancher": {
|
"rancher": {
|
||||||
"annotations": {
|
"annotations": {
|
||||||
|
|||||||
@ -123,26 +123,5 @@ export default {
|
|||||||
const clusterId = currentCluster.id;
|
const clusterId = currentCluster.id;
|
||||||
|
|
||||||
return projectsInAllClusters.filter((project: any) => project.spec.clusterName === clusterId && project.nameDisplay !== 'System');
|
return projectsInAllClusters.filter((project: any) => project.spec.clusterName === clusterId && project.nameDisplay !== 'System');
|
||||||
},
|
}
|
||||||
|
|
||||||
// Few harvester resources name and REAL resource are different. E.g. HCI.NETWORK_ATTACHMENT page resource is NETWORK_ATTACHMENT.
|
|
||||||
// Check in config/harvester-cluster.js for more details.
|
|
||||||
// We need to look up the schema by resource name, and fallback to find using real resource name
|
|
||||||
schemaFor: (state, getters, rootState, rootGetters) => (type, _fuzzy = false, _allowThrow = true) => {
|
|
||||||
// follow the same logic as type-map/schemaFor in /dashboard/shell/plugins/dashboard-store/getters.js
|
|
||||||
const normalizedType = getters.normalizeType(type);
|
|
||||||
const schemas = state.types['schema'];
|
|
||||||
const out = schemas?.map?.get(normalizedType);
|
|
||||||
|
|
||||||
if (out) return out;
|
|
||||||
|
|
||||||
// if not found, use the resource mapping in configureType for a second try
|
|
||||||
const resourceType = rootGetters['type-map/optionsFor'](type)?.resource;
|
|
||||||
if (resourceType && resourceType !== type) {
|
|
||||||
const normalizedResource = getters.normalizeType(resourceType);
|
|
||||||
return schemas?.map?.get(normalizedResource) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,21 +0,0 @@
|
|||||||
const AUTH_ERROR_CODES = [401, 403, 404];
|
|
||||||
|
|
||||||
export function getLoginAwareErrors(err, message = '') {
|
|
||||||
const errors = Array.isArray(err) ? err : (err ? [err] : []);
|
|
||||||
|
|
||||||
if (!errors.length) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const generic = message;
|
|
||||||
|
|
||||||
if (errors.some((e) => AUTH_ERROR_CODES.includes(e?._status || e?.response?.status))) {
|
|
||||||
return [generic];
|
|
||||||
}
|
|
||||||
|
|
||||||
const msgs = errors
|
|
||||||
.map((e) => (typeof e === 'string' ? e : (e?.message || e?._statusText || '')))
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
return msgs.length ? msgs : [generic];
|
|
||||||
}
|
|
||||||
@ -27,6 +27,23 @@ OUTPUT_DIR=dist/${DIR}-embedded
|
|||||||
echo "Building..."
|
echo "Building..."
|
||||||
COMMIT=${COMMIT} VERSION=${VERSION} OUTPUT_DIR=$OUTPUT_DIR ROUTER_BASE='/dashboard/' RESOURCE_BASE='/dashboard/' RANCHER_ENV=harvester yarn run build
|
COMMIT=${COMMIT} VERSION=${VERSION} OUTPUT_DIR=$OUTPUT_DIR ROUTER_BASE='/dashboard/' RESOURCE_BASE='/dashboard/' RANCHER_ENV=harvester yarn run build
|
||||||
|
|
||||||
|
if [ -v EMBED_PKG ]; then
|
||||||
|
echo "Build and embed plugin from: $EMBED_PKG"
|
||||||
|
PKG_FILE_NAME=${EMBED_PKG##*/}
|
||||||
|
echo PKG_FILE_NAME: $PKG_FILE_NAME
|
||||||
|
|
||||||
|
PKG_NAME="${PKG_FILE_NAME/.tar.gz/""}"
|
||||||
|
echo "Plugin name: '$PKG_NAME'"
|
||||||
|
|
||||||
|
# Fetch file, unpack and move to dist
|
||||||
|
curl $EMBED_PKG --output $PKG_FILE_NAME
|
||||||
|
OUTPUT_DIR_PKG=$OUTPUT_DIR/$PKG_NAME
|
||||||
|
mkdir -p $OUTPUT_DIR_PKG
|
||||||
|
tar xvfz $PKG_FILE_NAME -C $OUTPUT_DIR/$PKG_NAME
|
||||||
|
echo "Plugin contents that will be served from $PKG_NAME"
|
||||||
|
ls -alR $OUTPUT_DIR/$PKG_NAME
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Destroying..."
|
echo "Destroying..."
|
||||||
find $OUTPUT_DIR -name "index.html" -mindepth 2 -exec rm {} \;
|
find $OUTPUT_DIR -name "index.html" -mindepth 2 -exec rm {} \;
|
||||||
find $OUTPUT_DIR -type d -empty -depth -exec rmdir {} \;
|
find $OUTPUT_DIR -type d -empty -depth -exec rmdir {} \;
|
||||||
|
|||||||
12
yarn.lock
12
yarn.lock
@ -7148,9 +7148,9 @@ eslint-plugin-node@^11.1.0:
|
|||||||
semver "^6.1.0"
|
semver "^6.1.0"
|
||||||
|
|
||||||
eslint-plugin-promise@^7.1.0:
|
eslint-plugin-promise@^7.1.0:
|
||||||
version "7.3.0"
|
version "7.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-7.3.0.tgz#7c61e117f5db8d7a300bd5143c15d1d828e4c124"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-7.2.1.tgz#a0652195700aea40b926dc3c74b38e373377bfb0"
|
||||||
integrity sha512-6uGiOR0INuujr6PEQmeSSP7GbIMJ/ebEXXiEzb/nOj68LknH5Pxzb/AbZivmr6VE6TkTE8rTjRK9zhKpK6HsRA==
|
integrity sha512-SWKjd+EuvWkYaS+uN2csvj0KoP43YTu7+phKQ5v+xw6+A0gutVX2yqCeCkC3uLCJFiPfR2dD8Es5L7yUsmvEaA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@eslint-community/eslint-utils" "^4.4.0"
|
"@eslint-community/eslint-utils" "^4.4.0"
|
||||||
|
|
||||||
@ -13838,9 +13838,9 @@ yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2:
|
|||||||
integrity sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==
|
integrity sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==
|
||||||
|
|
||||||
yaml@^2.5.1:
|
yaml@^2.5.1:
|
||||||
version "2.8.4"
|
version "2.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.4.tgz#4b5f411dd25f9544914d8673d4da7f29248e5e2e"
|
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.3.tgz#a0d6bd2efb3dd03c59370223701834e60409bd7d"
|
||||||
integrity sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==
|
integrity sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==
|
||||||
|
|
||||||
yargs-parser@^18.1.2:
|
yargs-parser@^18.1.2:
|
||||||
version "18.1.3"
|
version "18.1.3"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user