mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-03-23 13:41:45 +00:00
Compare commits
5 Commits
main
...
v1.8.0-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8111f0ad7 | ||
|
|
6d627f82e9 | ||
|
|
cfc7a76fe7 | ||
|
|
71d3067354 | ||
|
|
9ecc372009 |
@ -7,7 +7,7 @@ The Harvester UI Extension is a Rancher extension that provides the user interfa
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
|
For detailed installation instructions, please refer to the [official Harvester documentation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210).
|
||||||
|
|
||||||
|
|
||||||
## Development Setup
|
## Development Setup
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.8.0-rc2",
|
"version": "1.8.0-rc1",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
|
|||||||
@ -7,7 +7,8 @@ The Harvester UI Extension is a Rancher extension that provides the user interfa
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
|
For detailed installation instructions, please refer to the [official Harvester documentation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210).
|
||||||
|
|
||||||
|
|
||||||
## Development Setup
|
## Development Setup
|
||||||
|
|
||||||
|
|||||||
@ -1,104 +0,0 @@
|
|||||||
<script>
|
|
||||||
import UnitInput from '@shell/components/form/UnitInput';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterInstanceManagerResources',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
UnitInput,
|
|
||||||
Banner,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({
|
|
||||||
value: '',
|
|
||||||
default: '{}'
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
const resources = this.parseJSON(this.value?.value) || this.parseJSON(this.value?.default) || {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
resources,
|
|
||||||
parseError: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
parseJSON(string) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(string);
|
|
||||||
} catch (e) {
|
|
||||||
this.parseError = this.t('harvester.setting.instanceManagerResources.parseError', { error: e.message });
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
update() {
|
|
||||||
if (!this.value) return;
|
|
||||||
|
|
||||||
const cpu = { ...this.resources?.cpu };
|
|
||||||
|
|
||||||
if (cpu.v1 !== null) cpu.v1 = String(cpu.v1);
|
|
||||||
if (cpu.v2 !== null) cpu.v2 = String(cpu.v2);
|
|
||||||
|
|
||||||
this.value.value = JSON.stringify({ ...this.resources, cpu });
|
|
||||||
},
|
|
||||||
|
|
||||||
useDefault() {
|
|
||||||
if (this.value?.default) {
|
|
||||||
this.resources = this.parseJSON(this.value.default) || {};
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<Banner
|
|
||||||
v-if="parseError"
|
|
||||||
color="error"
|
|
||||||
>
|
|
||||||
{{ parseError }}
|
|
||||||
</Banner>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="resources.cpu.v1"
|
|
||||||
:label="t('harvester.setting.instanceManagerResources.v1')"
|
|
||||||
suffix="%"
|
|
||||||
:delay="0"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
required
|
|
||||||
:mode="mode"
|
|
||||||
class="mb-20"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="resources.cpu.v2"
|
|
||||||
:label="t('harvester.setting.instanceManagerResources.v2')"
|
|
||||||
suffix="%"
|
|
||||||
:delay="0"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
required
|
|
||||||
:mode="mode"
|
|
||||||
class="mb-20"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -1,371 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
|
||||||
import ArrayList from '@shell/components/form/ArrayList';
|
|
||||||
import { isValidCIDR } from '@shell/utils/validators/cidr';
|
|
||||||
import { _EDIT } from '@shell/config/query-params';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
|
||||||
import { HCI } from '../../types';
|
|
||||||
import { NETWORK_TYPE } from '../../config/types';
|
|
||||||
|
|
||||||
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
|
|
||||||
const SHARE_STORAGE_NETWORK = 'share-storage-network';
|
|
||||||
const NETWORK = 'network';
|
|
||||||
|
|
||||||
const DEFAULT_DEDICATED_NETWORK = {
|
|
||||||
vlan: '',
|
|
||||||
clusterNetwork: '',
|
|
||||||
range: '',
|
|
||||||
exclude: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'RwxNetworkSetting',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
RadioGroup,
|
|
||||||
Banner,
|
|
||||||
ArrayList,
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
registerBeforeHook: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
default: _EDIT,
|
|
||||||
},
|
|
||||||
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
await allHash({
|
|
||||||
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
|
|
||||||
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
let enabled = false; // enabled / disabled options
|
|
||||||
let shareStorageNetwork = false; // shareStorageNetwork / dedicatedRwxNetwork options
|
|
||||||
let dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
|
||||||
let networkType = L2VLAN;
|
|
||||||
let exclude = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsedValue = JSON.parse(this.value.value || this.value.default || '{}');
|
|
||||||
const parsedNetwork = parsedValue?.[NETWORK] || parsedValue || {};
|
|
||||||
|
|
||||||
if (parsedValue && typeof parsedValue === 'object') {
|
|
||||||
shareStorageNetwork = !!parsedValue[SHARE_STORAGE_NETWORK];
|
|
||||||
networkType = 'vlan' in parsedNetwork ? L2VLAN : UNTAGGED;
|
|
||||||
dedicatedNetwork = {
|
|
||||||
vlan: parsedNetwork.vlan || '',
|
|
||||||
clusterNetwork: parsedNetwork.clusterNetwork || '',
|
|
||||||
range: parsedNetwork.range || '',
|
|
||||||
};
|
|
||||||
exclude = parsedNetwork?.exclude?.toString().split(',') || [];
|
|
||||||
enabled = shareStorageNetwork || !!(parsedNetwork.vlan || parsedNetwork.clusterNetwork || parsedNetwork.range);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
enabled = false;
|
|
||||||
shareStorageNetwork = false;
|
|
||||||
dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabled,
|
|
||||||
shareStorageNetwork,
|
|
||||||
dedicatedNetwork,
|
|
||||||
networkType,
|
|
||||||
exclude,
|
|
||||||
defaultAddValue: '',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
if (this.registerBeforeHook) {
|
|
||||||
this.registerBeforeHook(this.willSave, 'willSave');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
showDedicatedNetworkConfig() {
|
|
||||||
return this.enabled && !this.shareStorageNetwork;
|
|
||||||
},
|
|
||||||
|
|
||||||
showVlan() {
|
|
||||||
return this.networkType === L2VLAN;
|
|
||||||
},
|
|
||||||
|
|
||||||
networkTypes() {
|
|
||||||
return [L2VLAN, UNTAGGED];
|
|
||||||
},
|
|
||||||
|
|
||||||
clusterNetworkOptions() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
|
||||||
const clusterNetworksOptions = this.networkType === UNTAGGED ? clusterNetworks.filter((network) => network.id !== 'mgmt') : clusterNetworks;
|
|
||||||
|
|
||||||
return clusterNetworksOptions.map((network) => {
|
|
||||||
const disabled = !network.isReadyForStorageNetwork;
|
|
||||||
|
|
||||||
return {
|
|
||||||
label: disabled ? `${ network.id } (${ this.t('generic.notReady') })` : network.id,
|
|
||||||
value: network.id,
|
|
||||||
disabled,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
onUpdateEnabled() {
|
|
||||||
if (!this.enabled) {
|
|
||||||
this.shareStorageNetwork = false;
|
|
||||||
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
|
||||||
}
|
|
||||||
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
onUpdateNetworkType() {
|
|
||||||
if (this.shareStorageNetwork) {
|
|
||||||
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
|
||||||
}
|
|
||||||
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
onUpdateDedicatedType(neu) {
|
|
||||||
this.dedicatedNetwork.clusterNetwork = '';
|
|
||||||
|
|
||||||
if (neu === L2VLAN) {
|
|
||||||
this.dedicatedNetwork.vlan = '';
|
|
||||||
} else {
|
|
||||||
delete this.dedicatedNetwork.vlan;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
inputVlan(neu) {
|
|
||||||
if (neu === '') {
|
|
||||||
this.dedicatedNetwork.vlan = '';
|
|
||||||
this.update();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newValue = Number(neu);
|
|
||||||
|
|
||||||
if (newValue > 4094) {
|
|
||||||
this.dedicatedNetwork.vlan = 4094;
|
|
||||||
} else if (newValue < 1) {
|
|
||||||
this.dedicatedNetwork.vlan = 1;
|
|
||||||
} else {
|
|
||||||
this.dedicatedNetwork.vlan = newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
useDefault() {
|
|
||||||
this.enabled = false;
|
|
||||||
this.shareStorageNetwork = false;
|
|
||||||
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
update() {
|
|
||||||
const value = { [SHARE_STORAGE_NETWORK]: false };
|
|
||||||
|
|
||||||
if (this.enabled && this.shareStorageNetwork) {
|
|
||||||
value[SHARE_STORAGE_NETWORK] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.showDedicatedNetworkConfig) {
|
|
||||||
value[NETWORK] = {};
|
|
||||||
|
|
||||||
if (this.showVlan) {
|
|
||||||
value[NETWORK].vlan = this.dedicatedNetwork.vlan;
|
|
||||||
}
|
|
||||||
|
|
||||||
value[NETWORK].clusterNetwork = this.dedicatedNetwork.clusterNetwork;
|
|
||||||
value[NETWORK].range = this.dedicatedNetwork.range;
|
|
||||||
|
|
||||||
const excludeList = this.exclude.filter((ip) => ip);
|
|
||||||
|
|
||||||
if (Array.isArray(excludeList) && excludeList.length > 0) {
|
|
||||||
value[NETWORK].exclude = excludeList;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value.value = JSON.stringify(value);
|
|
||||||
},
|
|
||||||
|
|
||||||
willSave() {
|
|
||||||
this.update();
|
|
||||||
|
|
||||||
if (!this.showDedicatedNetworkConfig) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
if (this.showVlan && !this.dedicatedNetwork.vlan) {
|
|
||||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.dedicatedNetwork.clusterNetwork) {
|
|
||||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.clusterNetwork') }, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.dedicatedNetwork.range) {
|
|
||||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.range.label') }, true));
|
|
||||||
} else if (!isValidCIDR(this.dedicatedNetwork.range)) {
|
|
||||||
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.exclude) {
|
|
||||||
const hasInvalidCIDR = this.exclude.find((cidr) => {
|
|
||||||
return cidr && !isValidCIDR(cidr);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (hasInvalidCIDR) {
|
|
||||||
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', null, true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
return Promise.reject(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div :class="mode">
|
|
||||||
<Banner color="warning">
|
|
||||||
<t
|
|
||||||
k="harvester.setting.rwxNetwork.warning"
|
|
||||||
:raw="true"
|
|
||||||
/>
|
|
||||||
</Banner>
|
|
||||||
<RadioGroup
|
|
||||||
v-model:value="enabled"
|
|
||||||
class="mb-20"
|
|
||||||
name="rwx-network-enable"
|
|
||||||
:options="[true,false]"
|
|
||||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
|
||||||
@update:value="onUpdateEnabled"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RadioGroup
|
|
||||||
v-if="enabled"
|
|
||||||
v-model:value="shareStorageNetwork"
|
|
||||||
class="mb-20"
|
|
||||||
name="rwx-network-type"
|
|
||||||
:options="[true,false]"
|
|
||||||
:labels="[t('harvester.setting.rwxNetwork.shareStorageNetwork'), t('harvester.setting.rwxNetwork.dedicatedRwxNetwork')]"
|
|
||||||
@update:value="onUpdateNetworkType"
|
|
||||||
/>
|
|
||||||
<Banner
|
|
||||||
v-if="shareStorageNetwork"
|
|
||||||
class="mb-20"
|
|
||||||
color="warning"
|
|
||||||
>
|
|
||||||
<t
|
|
||||||
k="harvester.setting.rwxNetwork.shareStorageNetworkWarning"
|
|
||||||
:raw="true"
|
|
||||||
/>
|
|
||||||
</Banner>
|
|
||||||
<template v-if="showDedicatedNetworkConfig">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="networkType"
|
|
||||||
class="mb-20"
|
|
||||||
:options="networkTypes"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.fields.type')"
|
|
||||||
required
|
|
||||||
@update:value="onUpdateDedicatedType"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledInput
|
|
||||||
v-if="showVlan"
|
|
||||||
v-model:value.number="dedicatedNetwork.vlan"
|
|
||||||
type="number"
|
|
||||||
class="mb-20"
|
|
||||||
:mode="mode"
|
|
||||||
required
|
|
||||||
placeholder="e.g. 1 - 4094"
|
|
||||||
label-key="harvester.setting.storageNetwork.vlan"
|
|
||||||
@update:value="inputVlan"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="dedicatedNetwork.clusterNetwork"
|
|
||||||
label-key="harvester.setting.storageNetwork.clusterNetwork"
|
|
||||||
class="mb-20"
|
|
||||||
required
|
|
||||||
:options="clusterNetworkOptions"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="dedicatedNetwork.range"
|
|
||||||
class="mb-5"
|
|
||||||
:mode="mode"
|
|
||||||
required
|
|
||||||
:placeholder="t('harvester.setting.storageNetwork.range.placeholder')"
|
|
||||||
label-key="harvester.setting.storageNetwork.range.label"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ArrayList
|
|
||||||
v-model:value="exclude"
|
|
||||||
:show-header="true"
|
|
||||||
:default-add-value="defaultAddValue"
|
|
||||||
:mode="mode"
|
|
||||||
:add-label="t('harvester.setting.storageNetwork.exclude.addIp')"
|
|
||||||
class="mt-20"
|
|
||||||
@update:value="update"
|
|
||||||
>
|
|
||||||
<template #column-headers>
|
|
||||||
<div class="box mb-10">
|
|
||||||
<div class="key">
|
|
||||||
{{ t('harvester.setting.storageNetwork.exclude.label') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #columns="scope">
|
|
||||||
<div class="key">
|
|
||||||
<input
|
|
||||||
v-model="scope.row.value"
|
|
||||||
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ArrayList>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -63,8 +63,6 @@ const FEATURE_FLAGS = {
|
|||||||
'supportBundleFileNameSetting',
|
'supportBundleFileNameSetting',
|
||||||
'clusterRegistrationTLSVerify',
|
'clusterRegistrationTLSVerify',
|
||||||
'vGPUAsPCIDevice',
|
'vGPUAsPCIDevice',
|
||||||
'instanceManagerResourcesSetting',
|
|
||||||
'rwxNetworkSetting',
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,6 @@ export const HCI_SETTING = {
|
|||||||
SUPPORT_BUNDLE_IMAGE: 'support-bundle-image',
|
SUPPORT_BUNDLE_IMAGE: 'support-bundle-image',
|
||||||
SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT: 'support-bundle-node-collection-timeout',
|
SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT: 'support-bundle-node-collection-timeout',
|
||||||
STORAGE_NETWORK: 'storage-network',
|
STORAGE_NETWORK: 'storage-network',
|
||||||
RWX_NETWORK: 'rwx-network',
|
|
||||||
VM_FORCE_RESET_POLICY: 'vm-force-reset-policy',
|
VM_FORCE_RESET_POLICY: 'vm-force-reset-policy',
|
||||||
SSL_CERTIFICATES: 'ssl-certificates',
|
SSL_CERTIFICATES: 'ssl-certificates',
|
||||||
SSL_PARAMETERS: 'ssl-parameters',
|
SSL_PARAMETERS: 'ssl-parameters',
|
||||||
@ -40,8 +39,7 @@ export const HCI_SETTING = {
|
|||||||
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
||||||
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'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_ALLOWED_SETTINGS = {
|
export const HCI_ALLOWED_SETTINGS = {
|
||||||
@ -82,9 +80,6 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
[HCI_SETTING.STORAGE_NETWORK]: {
|
[HCI_SETTING.STORAGE_NETWORK]: {
|
||||||
kind: 'custom', from: 'import', canReset: true
|
kind: 'custom', from: 'import', canReset: true
|
||||||
},
|
},
|
||||||
[HCI_SETTING.RWX_NETWORK]: {
|
|
||||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'rwxNetworkSetting'
|
|
||||||
},
|
|
||||||
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.SSL_PARAMETERS]: {
|
[HCI_SETTING.SSL_PARAMETERS]: {
|
||||||
@ -127,9 +122,6 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
},
|
},
|
||||||
[HCI_SETTING.KUBEVIRT_MIGRATION]: {
|
[HCI_SETTING.KUBEVIRT_MIGRATION]: {
|
||||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'kubevirtMigration',
|
kind: 'json', from: 'import', canReset: true, featureFlag: 'kubevirtMigration',
|
||||||
},
|
|
||||||
[HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: {
|
|
||||||
kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting'
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,239 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
import { PVC } from '@shell/config/types';
|
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
|
||||||
import { sortBy } from '@shell/utils/sort';
|
|
||||||
import { HCI } from '../types';
|
|
||||||
import { parseVolumeClaimTemplates } from '@pkg/harvester/utils/vm';
|
|
||||||
|
|
||||||
import { Card } from '@components/Card';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterStorageMigrationDialog',
|
|
||||||
|
|
||||||
emits: ['close'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
AsyncButton, Banner, Card, LabeledSelect
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
resources: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
this.allPVCs = await this.$store.dispatch('harvester/findAll', { type: PVC });
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
sourceVolume: '',
|
|
||||||
targetVolume: '',
|
|
||||||
errors: [],
|
|
||||||
allPVCs: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
actionResource() {
|
|
||||||
return this.resources[0];
|
|
||||||
},
|
|
||||||
|
|
||||||
sourceVolumeOptions() {
|
|
||||||
const volumes = this.actionResource.spec?.template?.spec?.volumes || [];
|
|
||||||
|
|
||||||
return sortBy(
|
|
||||||
volumes
|
|
||||||
.map((v) => v.persistentVolumeClaim?.claimName)
|
|
||||||
.filter((name) => !!name)
|
|
||||||
.map((name) => ({
|
|
||||||
label: name,
|
|
||||||
value: name
|
|
||||||
})),
|
|
||||||
'label'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
namespacePVCs() {
|
|
||||||
return this.allPVCs.filter((pvc) => pvc.metadata.namespace === this.actionResource.metadata.namespace);
|
|
||||||
},
|
|
||||||
|
|
||||||
vmUsedVolumeNames() {
|
|
||||||
const allVMs = this.$store.getters['harvester/all'](HCI.VM) || [];
|
|
||||||
const names = new Set();
|
|
||||||
|
|
||||||
allVMs.forEach((vm) => {
|
|
||||||
// Collect volume names from spec.template.spec.volumes (both PVC and DataVolume references)
|
|
||||||
const volumes = vm.spec?.template?.spec?.volumes || [];
|
|
||||||
|
|
||||||
volumes.forEach((v) => {
|
|
||||||
const name = v.persistentVolumeClaim?.claimName || v.dataVolume?.name;
|
|
||||||
|
|
||||||
if (name) {
|
|
||||||
names.add(`${ vm.metadata.namespace }/${ name }`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Collect volume names from volumeClaimTemplates annotation
|
|
||||||
const templates = parseVolumeClaimTemplates(vm);
|
|
||||||
|
|
||||||
templates.forEach((t) => {
|
|
||||||
if (t.metadata?.name) {
|
|
||||||
names.add(`${ vm.metadata.namespace }/${ t.metadata.name }`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return names;
|
|
||||||
},
|
|
||||||
|
|
||||||
targetVolumeOptions() {
|
|
||||||
return sortBy(
|
|
||||||
this.namespacePVCs
|
|
||||||
.filter((pvc) => {
|
|
||||||
// Exclude volumes used by any VM (via spec.volumes or volumeClaimTemplates)
|
|
||||||
if (this.vmUsedVolumeNames.has(`${ pvc.metadata.namespace }/${ pvc.metadata.name }`)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.map((pvc) => ({
|
|
||||||
label: pvc.metadata.name,
|
|
||||||
value: pvc.metadata.name
|
|
||||||
})),
|
|
||||||
'label'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
disableSave() {
|
|
||||||
return !this.sourceVolume || !this.targetVolume;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.sourceVolume = '';
|
|
||||||
this.targetVolume = '';
|
|
||||||
this.errors = [];
|
|
||||||
this.$emit('close');
|
|
||||||
},
|
|
||||||
|
|
||||||
async apply(buttonDone) {
|
|
||||||
if (!this.actionResource) {
|
|
||||||
buttonDone(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.sourceVolume) {
|
|
||||||
const name = this.t('harvester.modal.storageMigration.fields.sourceVolume.label');
|
|
||||||
|
|
||||||
this['errors'] = [this.t('validation.required', { key: name })];
|
|
||||||
buttonDone(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.targetVolume) {
|
|
||||||
const name = this.t('harvester.modal.storageMigration.fields.targetVolume.label');
|
|
||||||
|
|
||||||
this['errors'] = [this.t('validation.required', { key: name })];
|
|
||||||
buttonDone(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.actionResource.doAction('storageMigration', {
|
|
||||||
sourceVolume: this.sourceVolume,
|
|
||||||
targetVolume: this.targetVolume
|
|
||||||
}, {}, false);
|
|
||||||
|
|
||||||
buttonDone(true);
|
|
||||||
this.close();
|
|
||||||
} catch (err) {
|
|
||||||
const error = err?.data || err;
|
|
||||||
|
|
||||||
this['errors'] = exceptionToErrorsArray(error);
|
|
||||||
buttonDone(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Card :show-highlight-border="false">
|
|
||||||
<template #title>
|
|
||||||
{{ t('harvester.modal.storageMigration.title') }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="sourceVolume"
|
|
||||||
:label="t('harvester.modal.storageMigration.fields.sourceVolume.label')"
|
|
||||||
:placeholder="t('harvester.modal.storageMigration.fields.sourceVolume.placeholder')"
|
|
||||||
:options="sourceVolumeOptions"
|
|
||||||
class="mb-20"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="targetVolume"
|
|
||||||
:label="t('harvester.modal.storageMigration.fields.targetVolume.label')"
|
|
||||||
:placeholder="t('harvester.modal.storageMigration.fields.targetVolume.placeholder')"
|
|
||||||
:options="targetVolumeOptions"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template
|
|
||||||
#actions
|
|
||||||
class="actions"
|
|
||||||
>
|
|
||||||
<div class="buttons">
|
|
||||||
<button
|
|
||||||
class="btn role-secondary mr-10"
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
{{ t('generic.cancel') }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<AsyncButton
|
|
||||||
mode="apply"
|
|
||||||
:disabled="disableSave"
|
|
||||||
@click="apply"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -123,15 +123,6 @@ harvester:
|
|||||||
namespace: Namespace
|
namespace: Namespace
|
||||||
message:
|
message:
|
||||||
success: 'Image { name } created successfully.'
|
success: 'Image { name } created successfully.'
|
||||||
storageMigration:
|
|
||||||
title: Storage Migration
|
|
||||||
fields:
|
|
||||||
sourceVolume:
|
|
||||||
label: Source Volume
|
|
||||||
placeholder: Select a source volume
|
|
||||||
targetVolume:
|
|
||||||
label: Target Volume
|
|
||||||
placeholder: Select a target volume
|
|
||||||
migration:
|
migration:
|
||||||
failedMessage: Latest migration failed!
|
failedMessage: Latest migration failed!
|
||||||
title: Migration
|
title: Migration
|
||||||
@ -241,8 +232,6 @@ harvester:
|
|||||||
migrate: Migrate
|
migrate: Migrate
|
||||||
cpuAndMemoryHotplug: Edit CPU and Memory
|
cpuAndMemoryHotplug: Edit CPU and Memory
|
||||||
abortMigration: Abort Migration
|
abortMigration: Abort Migration
|
||||||
storageMigration: Storage Migration
|
|
||||||
cancelStorageMigration: Cancel Storage Migration
|
|
||||||
createTemplate: Generate Template
|
createTemplate: Generate Template
|
||||||
enableMaintenance: Enable Maintenance Mode
|
enableMaintenance: Enable Maintenance Mode
|
||||||
disableMaintenance: Disable Maintenance Mode
|
disableMaintenance: Disable Maintenance Mode
|
||||||
@ -1280,11 +1269,6 @@ harvester:
|
|||||||
addIp: Add Exclude IP
|
addIp: Add Exclude IP
|
||||||
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
||||||
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
|
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
|
||||||
rwxNetwork:
|
|
||||||
warning: 'WARNING: <br/> Any change to rwx-network requires longhorn RWX volumes detached before applying this setting.<br/>Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
|
||||||
shareStorageNetwork: Share Storage Network
|
|
||||||
dedicatedRwxNetwork: Dedicated RWX Network
|
|
||||||
shareStorageNetworkWarning: The rwx-network is governed by storage-network, and changes here won't take effect until share-storage-network is set to false.
|
|
||||||
vmForceDeletionPolicy:
|
vmForceDeletionPolicy:
|
||||||
period: Period
|
period: Period
|
||||||
vmMigrationTimeout: VM Migration Timeout
|
vmMigrationTimeout: VM Migration Timeout
|
||||||
@ -1373,17 +1357,13 @@ harvester:
|
|||||||
url: URL
|
url: URL
|
||||||
insecureSkipTLSVerify: Insecure Skip TLS Verify
|
insecureSkipTLSVerify: Insecure Skip TLS Verify
|
||||||
tip:
|
tip:
|
||||||
prefix: Harvester secures cluster registration via TLS by default. If opt out "Insecure Skip TLS Verify", you must provide custom CA certificates using the
|
prefix: Harvester secures cluster registration via TLS by default. If opt out "Insecure SKip TLS Verify", you must provide custom CA certificates using the
|
||||||
middle: 'additional-ca'
|
middle: 'additional-ca'
|
||||||
suffix: setting.
|
suffix: setting.
|
||||||
message: To completely unset the imported Harvester cluster, please also remove it on the Rancher Dashboard UI via the <code> Virtualization Management </code> page.
|
message: To completely unset the imported Harvester cluster, please also remove it on the Rancher Dashboard UI via the <code> Virtualization Management </code> page.
|
||||||
ntpServers:
|
ntpServers:
|
||||||
isNotIPV4: The address you entered is not IPv4 or host. Please enter a valid IPv4 address or a host address.
|
isNotIPV4: The address you entered is not IPv4 or host. Please enter a valid IPv4 address or a host address.
|
||||||
isDuplicate: There are duplicate NTP server configurations.
|
isDuplicate: There are duplicate NTP server configurations.
|
||||||
instanceManagerResources:
|
|
||||||
parseError: "Failed to parse configuration: {error}"
|
|
||||||
v1: "V1 Data Engine"
|
|
||||||
v2: "V2 Data Engine"
|
|
||||||
kubevirtMigration:
|
kubevirtMigration:
|
||||||
parseError: "Failed to parse configuration: {error}"
|
parseError: "Failed to parse configuration: {error}"
|
||||||
parallelMigrationsPerCluster: "Parallel Migrations Per Cluster"
|
parallelMigrationsPerCluster: "Parallel Migrations Per Cluster"
|
||||||
@ -2003,7 +1983,6 @@ advancedSettings:
|
|||||||
'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down.
|
'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down.
|
||||||
'harv-ssl-parameters': Custom SSL Parameters for TLS validation.
|
'harv-ssl-parameters': Custom SSL Parameters for TLS validation.
|
||||||
'harv-storage-network': 'Longhorn storage-network setting.'
|
'harv-storage-network': 'Longhorn storage-network setting.'
|
||||||
'harv-rwx-network': 'Configure RWX network behavior for shared or dedicated storage network usage.'
|
|
||||||
'harv-support-bundle-namespaces': Select additional namespaces to include in the support bundle.
|
'harv-support-bundle-namespaces': Select additional namespaces to include in the support bundle.
|
||||||
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage.
|
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage.
|
||||||
'harv-support-bundle-image': Support bundle image configuration. Find different versions in <a href="https://hub.docker.com/r/rancher/support-bundle-kit/tags" target="_blank">rancher/support-bundle-kit</a>.
|
'harv-support-bundle-image': Support bundle image configuration. Find different versions in <a href="https://hub.docker.com/r/rancher/support-bundle-kit/tags" target="_blank">rancher/support-bundle-kit</a>.
|
||||||
@ -2019,7 +1998,6 @@ advancedSettings:
|
|||||||
'harv-rancher-cluster': 'Configure Rancher cluster integration settings for guest cluster management.'
|
'harv-rancher-cluster': 'Configure Rancher cluster integration settings for guest cluster management.'
|
||||||
'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.'
|
|
||||||
|
|
||||||
typeLabel:
|
typeLabel:
|
||||||
kubevirt.io.virtualmachine: |-
|
kubevirt.io.virtualmachine: |-
|
||||||
|
|||||||
@ -58,7 +58,7 @@ export default class PCIDevice extends SteveModel {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !!this.metadata?.labels?.[HCI_ANNOTATIONS.PARENT_SRIOV_GPU];
|
return !!this.metadata?.labels?.[HCI_ANNOTATIONS.PARENT_SRIOV_GPU] || this.status?.resourceName.includes('nvidia.com');
|
||||||
}
|
}
|
||||||
|
|
||||||
get canYaml() {
|
get canYaml() {
|
||||||
|
|||||||
@ -199,18 +199,6 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
icon: 'icon icon-close',
|
icon: 'icon icon-close',
|
||||||
label: this.t('harvester.action.abortMigration')
|
label: this.t('harvester.action.abortMigration')
|
||||||
},
|
},
|
||||||
{
|
|
||||||
action: 'storageMigration',
|
|
||||||
enabled: !!this.actions?.storageMigration,
|
|
||||||
icon: 'icon icon-copy',
|
|
||||||
label: this.t('harvester.action.storageMigration')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: 'cancelStorageMigration',
|
|
||||||
enabled: !!this.actions?.cancelStorageMigration,
|
|
||||||
icon: 'icon icon-close',
|
|
||||||
label: this.t('harvester.action.cancelStorageMigration')
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
action: 'addHotplugVolume',
|
action: 'addHotplugVolume',
|
||||||
enabled: !!this.actions?.addVolume,
|
enabled: !!this.actions?.addVolume,
|
||||||
@ -380,13 +368,6 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
storageMigration(resources = this) {
|
|
||||||
this.$dispatch('promptModal', {
|
|
||||||
resources,
|
|
||||||
component: 'HarvesterStorageMigrationDialog'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
backupVM(resources = this) {
|
backupVM(resources = this) {
|
||||||
this.$dispatch('promptModal', {
|
this.$dispatch('promptModal', {
|
||||||
resources,
|
resources,
|
||||||
@ -539,10 +520,6 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
this.doActionGrowl('abortMigration', {});
|
this.doActionGrowl('abortMigration', {});
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelStorageMigration() {
|
|
||||||
this.doActionGrowl('cancelStorageMigration', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
createTemplate(resources = this) {
|
createTemplate(resources = this) {
|
||||||
this.$dispatch('promptModal', {
|
this.$dispatch('promptModal', {
|
||||||
resources,
|
resources,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester",
|
"name": "harvester",
|
||||||
"description": "Rancher UI Extension for Harvester",
|
"description": "Rancher UI Extension for Harvester",
|
||||||
"version": "1.8.0-rc2",
|
"version": "1.8.0-rc1",
|
||||||
"private": false,
|
"private": false,
|
||||||
"rancher": {
|
"rancher": {
|
||||||
"annotations": {
|
"annotations": {
|
||||||
|
|||||||
@ -13844,9 +13844,9 @@ yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2:
|
|||||||
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
||||||
|
|
||||||
yaml@^2.5.1:
|
yaml@^2.5.1:
|
||||||
version "2.8.3"
|
version "2.8.2"
|
||||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.3.tgz#a0d6bd2efb3dd03c59370223701834e60409bd7d"
|
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.2.tgz#5694f25eca0ce9c3e7a9d9e00ce0ddabbd9e35c5"
|
||||||
integrity sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==
|
integrity sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==
|
||||||
|
|
||||||
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