mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-01 11:46:06 +08:00
Compare commits
234
Commits
v1.0.6
..
v1.5.2-rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e0cea8986 | ||
|
|
af80479bd9 | ||
|
|
4caecb48a7 | ||
|
|
8d1d370197 | ||
|
|
df9992146c | ||
|
|
acbc120dc1 | ||
|
|
59c63c8d9e | ||
|
|
4a4f2c96bb | ||
|
|
d7bc112a64 | ||
|
|
08e81b0628 | ||
|
|
4599159bff | ||
|
|
ef40cf5d8d | ||
|
|
d57dbd4c8e | ||
|
|
4c68d057c1 | ||
|
|
b4b2017c61 | ||
|
|
9f0d927c9c | ||
|
|
18a53f5baf | ||
|
|
f872bb5abf | ||
|
|
cd015d57e7 | ||
|
|
635254afdc | ||
|
|
3c464e9d10 | ||
|
|
d412ee2aa6 | ||
|
|
580b2892cb | ||
|
|
965c7d9b72 | ||
|
|
01e3867da1 | ||
|
|
fd52218350 | ||
|
|
1839bf387e | ||
|
|
3e9a0a4377 | ||
|
|
a2898a2554 | ||
|
|
d11a045aed | ||
|
|
ae2a2f6d28 | ||
|
|
cf68c3976a | ||
|
|
33f8300c0e | ||
|
|
b26e34a2bd | ||
|
|
01fd3e5e61 | ||
|
|
d5df7ba602 | ||
|
|
607aa106e1 | ||
|
|
49e2be6ff4 | ||
|
|
9ee81f4054 | ||
|
|
f8fa82f614 | ||
|
|
a6c60de2cb | ||
|
|
843b388ede | ||
|
|
f2e8cce29f | ||
|
|
31682dd165 | ||
|
|
9fac43d5e5 | ||
|
|
cd6ea6e402 | ||
|
|
3bfe57dc54 | ||
|
|
089573f193 | ||
|
|
5d0c4f0c5a | ||
|
|
57bb1e8562 | ||
|
|
30ff129691 | ||
|
|
9175f16bfc | ||
|
|
7c5e0f93d1 | ||
|
|
8a7d3dffbe | ||
|
|
b7810c5970 | ||
|
|
72dedd46d1 | ||
|
|
24f716ab20 | ||
|
|
38ad0fe1a7 | ||
|
|
c84d262099 | ||
|
|
dbb53eae00 | ||
|
|
4afacd754a | ||
|
|
10d159e2c7 | ||
|
|
de755a662a | ||
|
|
f1200f4508 | ||
|
|
0ca2b0591a | ||
|
|
44f003309c | ||
|
|
7c1596c965 | ||
|
|
a5130986a8 | ||
|
|
e4ae7cec5e | ||
|
|
2c7b59daed | ||
|
|
224e34315b | ||
|
|
9f56f9eccc | ||
|
|
dd76956972 | ||
|
|
d92a53d9a7 | ||
|
|
afc3f0d87f | ||
|
|
a607b604f8 | ||
|
|
4d7b0f7350 | ||
|
|
c570b5870b | ||
|
|
af36163b29 | ||
|
|
b3dbc2f0d3 | ||
|
|
fed86d4c24 | ||
|
|
d18ebfb26b | ||
|
|
477206d8d0 | ||
|
|
fd0836d489 | ||
|
|
2e3536abc6 | ||
|
|
45c0dce61b | ||
|
|
6b8b1f68eb | ||
|
|
bb645c00db | ||
|
|
ed8c6e81ca | ||
|
|
ed692b579d | ||
|
|
f02074a25e | ||
|
|
a8f8f0d288 | ||
|
|
9fa9b3837b | ||
|
|
4b3bdf8a59 | ||
|
|
7bdf40d737 | ||
|
|
a1e7345437 | ||
|
|
9322a77b20 | ||
|
|
ed7d7ffd2e | ||
|
|
2ef8e2e0aa | ||
|
|
a0ea6beedb | ||
|
|
85a723e20c | ||
|
|
bbe6b1bdf0 | ||
|
|
81a572e5b5 | ||
|
|
1a727f0001 | ||
|
|
91862244e1 | ||
|
|
c7b0e9ee7a | ||
|
|
d9337ff188 | ||
|
|
c16922c228 | ||
|
|
bbade7e50c | ||
|
|
193bf092d5 | ||
|
|
308b6ddbd7 | ||
|
|
c850b78da2 | ||
|
|
0fe9557eb8 | ||
|
|
b6953d73f7 | ||
|
|
8a89a9c502 | ||
|
|
d5f17bfdf1 | ||
|
|
9d0bb6e1f4 | ||
|
|
2ec37d81d9 | ||
|
|
e50bff8b5e | ||
|
|
e898181834 | ||
|
|
a0f877a23e | ||
|
|
a805490930 | ||
|
|
887a122358 | ||
|
|
1f48e8aea7 | ||
|
|
b2c1a3b068 | ||
|
|
df1566f394 | ||
|
|
0a3b122316 | ||
|
|
d497613cc4 | ||
|
|
91494f5832 | ||
|
|
c61d30a607 | ||
|
|
9428a07850 | ||
|
|
28603d3ab3 | ||
|
|
f863b881a9 | ||
|
|
083ab9f3e9 | ||
|
|
5d52c368a1 | ||
|
|
d72f379ab1 | ||
|
|
2931248fdb | ||
|
|
7166a5b1bb | ||
|
|
00c884cda0 | ||
|
|
1c65b71e35 | ||
|
|
3e4bc31a9d | ||
|
|
bc4bbd9a40 | ||
|
|
a6666ef384 | ||
|
|
92588c9adc | ||
|
|
832df26efc | ||
|
|
f7869cb241 | ||
|
|
27c0124682 | ||
|
|
6c69579c41 | ||
|
|
145b534c5d | ||
|
|
ce017a0e40 | ||
|
|
72a6f77fd2 | ||
|
|
275b15ad62 | ||
|
|
06c9a8b3d9 | ||
|
|
36b1406738 | ||
|
|
06b13a9cc5 | ||
|
|
2a3cd32447 | ||
|
|
40dc31226b | ||
|
|
def9e1a24e | ||
|
|
da25b28f7f | ||
|
|
10abbd0452 | ||
|
|
8a7cc69a69 | ||
|
|
c188f3367d | ||
|
|
26d85a7760 | ||
|
|
6eb845b46b | ||
|
|
8c20ddb440 | ||
|
|
a92f27d80c | ||
|
|
f523a161b6 | ||
|
|
ad2273b5ba | ||
|
|
171939e66b | ||
|
|
18bfbbaac1 | ||
|
|
e79f493cb6 | ||
|
|
e84c0fe886 | ||
|
|
bbfeb55af9 | ||
|
|
e7446e7e23 | ||
|
|
adacaf65fa | ||
|
|
7c761b2829 | ||
|
|
cc8532ff99 | ||
|
|
ede281a42d | ||
|
|
d34279d98f | ||
|
|
efd93c5e52 | ||
|
|
8d338b5f72 | ||
|
|
f59c81da31 | ||
|
|
cf6d44601e | ||
|
|
89b43060df | ||
|
|
7194d52000 | ||
|
|
a8ea0645e4 | ||
|
|
713173b0e4 | ||
|
|
6ed13bcec1 | ||
|
|
02519820d7 | ||
|
|
2604101eb2 | ||
|
|
1cb16f0c74 | ||
|
|
752cf2e93a | ||
|
|
f4ea7b6ea0 | ||
|
|
a6405989e9 | ||
|
|
fb8a81fd0f | ||
|
|
3a5ead3c2c | ||
|
|
def4b64cd3 | ||
|
|
a191b845d8 | ||
|
|
32a57d791a | ||
|
|
2a7e51ef4c | ||
|
|
f726fb99e5 | ||
|
|
9c19ae1f9b | ||
|
|
6ece2d0284 | ||
|
|
aec424b007 | ||
|
|
ff80b91311 | ||
|
|
52252c16f7 | ||
|
|
19cf1e9302 | ||
|
|
ddb8e8106b | ||
|
|
9af5ff7b12 | ||
|
|
fff6153f15 | ||
|
|
1ecac7adf4 | ||
|
|
408818904a | ||
|
|
b96637e6d9 | ||
|
|
8bdaca777f | ||
|
|
d278687e75 | ||
|
|
3ea8edb824 | ||
|
|
e33302050b | ||
|
|
8fefb59802 | ||
|
|
872aec1c10 | ||
|
|
8814bd4cda | ||
|
|
72a17cd9c2 | ||
|
|
8d79b69748 | ||
|
|
47760a8426 | ||
|
|
e6fc641a29 | ||
|
|
90b85d2be7 | ||
|
|
ee9575f710 | ||
|
|
7d97e6821c | ||
|
|
42fc9d8fae | ||
|
|
0f03fc0805 | ||
|
|
6d4a7cd1b1 | ||
|
|
f69d445ef7 | ||
|
|
d358a0a618 | ||
|
|
47841e49b1 | ||
|
|
57c7f0f856 |
@@ -2,6 +2,7 @@ pull_request_rules:
|
||||
- name: Automatically open backport PR to release-harvester-v1.5
|
||||
conditions:
|
||||
- base=main
|
||||
- label="require backport 1.5"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
|
||||
@@ -11,18 +11,19 @@ defaults:
|
||||
working-directory: ./
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
check-version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/lint
|
||||
- name: Check package version
|
||||
env:
|
||||
TAG_VERSION: ${{ github.event.release.tag_name }}
|
||||
run: ./scripts/check-version.sh $TAG_VERSION
|
||||
|
||||
build-and-push-extension-catalog:
|
||||
needs: lint
|
||||
needs: check-version
|
||||
uses: ./.github/workflows/build-extension-catalog.yml
|
||||
permissions:
|
||||
actions: write
|
||||
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
|
||||
|
||||
- name: Run Helm chart releaser
|
||||
uses: helm/chart-releaser-action@v1.4.1
|
||||
uses: helm/chart-releaser-action@v1.7.0
|
||||
with:
|
||||
charts_dir: ./charts
|
||||
env:
|
||||
|
||||
@@ -22,26 +22,27 @@ jobs:
|
||||
- name: Determine release tag
|
||||
id: determine_tag
|
||||
run: |
|
||||
if [[ "${{ github.ref_name }}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
||||
if [[ "${{ github.event.release.tag_name }}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
||||
RELEASE_TAG="harvester-${BASH_REMATCH[1]}"
|
||||
echo "${RELEASE_TAG}"
|
||||
echo "release_tag=${RELEASE_TAG}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Error: invalid tag format." && exit 1
|
||||
fi
|
||||
lint:
|
||||
check-version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Check package version
|
||||
env:
|
||||
TAG_VERSION: ${{ github.event.release.tag_name }}
|
||||
run: ./scripts/check-version.sh $TAG_VERSION
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/lint
|
||||
build-extension-charts:
|
||||
needs:
|
||||
- setup-release-tag
|
||||
- lint
|
||||
- check-version
|
||||
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
|
||||
permissions:
|
||||
actions: write
|
||||
|
||||
@@ -5,14 +5,20 @@ on:
|
||||
- v[1-9].*
|
||||
|
||||
jobs:
|
||||
build-validation:
|
||||
name: Build Test
|
||||
uses: ./.github/workflows/run-lint.yaml
|
||||
check-version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Check package version
|
||||
env:
|
||||
TAG_VERSION: ${{github.ref_name}}
|
||||
run: ./scripts/check-version.sh $TAG_VERSION
|
||||
|
||||
build:
|
||||
name: Build and Upload Package
|
||||
uses: ./.github/workflows/build-and-publish-standalone.yaml
|
||||
needs:
|
||||
- build-validation
|
||||
needs: check-version
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
@@ -8,20 +8,7 @@ From Rancher 2.10.0, Harvester integration is provided by this new Harvester UI
|
||||
|
||||
## Installation
|
||||
|
||||
To install the Harvester UI Extension in Rancher 2.10.0 and newer:
|
||||
|
||||
- Go to 'Extensions' in the Rancher UI (click on the jigsaw icon in the side bar)
|
||||
- From the three-dots menu in the upper right of the page, open the menu and select 'Manage Repositories'
|
||||
- From the Repositories page, click 'Create'
|
||||
- For name, enter 'harvester-ui-extension'
|
||||
- For Target, choose 'Git repository'
|
||||
- For Git Repo URL, enter `https://github.com/harvester/harvester-ui-extension`
|
||||
- For Git Branch, enter `gh-pages`
|
||||
- Click 'Create`
|
||||
- From the Repositories list, wait until the new Repository added updates to the 'Active' state
|
||||
- Go back to 'Extensions'
|
||||
- Click on the 'Available' tab if not already selected
|
||||
- Click on 'Install' for the Harvester UI Extension
|
||||
To install the Harvester UI Extension in Rancher 2.10.0 and newer, please refer to [installation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210) on harvester doc.
|
||||
|
||||
## Running for Development
|
||||
|
||||
@@ -70,7 +57,7 @@ For developers, after reading through the introduction on this page, head over t
|
||||
|
||||
License
|
||||
=======
|
||||
Copyright (c) 2014-2023 [Rancher Labs, Inc.](http://rancher.com)
|
||||
Copyright (c) 2014-2025 [SUSE, LLC.](https://www.suse.com/)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
+10
-10
@@ -1,36 +1,36 @@
|
||||
{
|
||||
"name": "harvester-ui-extension",
|
||||
"version": "1.0.6",
|
||||
"version": "1.5.2-rc2",
|
||||
"private": false,
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rancher/shell": "3.0.1-rc.4",
|
||||
"@rancher/shell": "3.0.2-rc.6",
|
||||
"cache-loader": "^4.1.0",
|
||||
"color": "4.2.3",
|
||||
"ip": "2.0.1",
|
||||
"node-polyfill-webpack-plugin": "^3.0.0",
|
||||
"vue-draggable-next": "^2.2.1",
|
||||
"yaml": "^2.5.1",
|
||||
"@babel/plugin-transform-class-static-block": "7.26.0"
|
||||
"@babel/plugin-transform-class-static-block": "7.28.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "~20.10.0",
|
||||
"cronstrue": "2.50.0",
|
||||
"@types/node": "~20.19.0",
|
||||
"cronstrue": "2.59.0",
|
||||
"d3-color": "3.1.0",
|
||||
"ejs": "3.1.10",
|
||||
"follow-redirects": "1.15.6",
|
||||
"follow-redirects": "1.15.11",
|
||||
"glob": "7.2.3",
|
||||
"glob-parent": "6.0.2",
|
||||
"json5": "2.2.3",
|
||||
"@types/lodash": "4.17.5",
|
||||
"@types/lodash": "4.17.20",
|
||||
"merge": "2.1.1",
|
||||
"node-forge": "1.3.1",
|
||||
"nth-check": "2.1.1",
|
||||
"qs": "6.11.1",
|
||||
"roarr": "7.0.4",
|
||||
"semver": "7.5.4",
|
||||
"qs": "6.14.0",
|
||||
"roarr": "7.21.1",
|
||||
"semver": "7.7.2",
|
||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -5,8 +5,10 @@ import { Checkbox } from '@components/Form/Checkbox';
|
||||
import ModalWithCard from '@shell/components/ModalWithCard';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import { HCI } from '../types';
|
||||
import UpgradeInfo from './UpgradeInfo';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterUpgrade',
|
||||
|
||||
@@ -28,14 +30,15 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
upgrade: [],
|
||||
upgradeMessage: [],
|
||||
errors: '',
|
||||
selectMode: true,
|
||||
version: '',
|
||||
enableLogging: true,
|
||||
readyReleaseNote: false,
|
||||
isOpen: false
|
||||
upgrade: [],
|
||||
upgradeMessage: [],
|
||||
errors: '',
|
||||
selectMode: true,
|
||||
version: '',
|
||||
enableLogging: true,
|
||||
skipSingleReplicaDetachedVol: false,
|
||||
readyReleaseNote: false,
|
||||
isOpen: false
|
||||
};
|
||||
},
|
||||
|
||||
@@ -68,6 +71,10 @@ export default {
|
||||
return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG);
|
||||
},
|
||||
|
||||
skipSingleReplicaDetachedVolFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
|
||||
},
|
||||
|
||||
releaseLink() {
|
||||
return `https://github.com/harvester/harvester/releases/tag/${ this.version }`;
|
||||
}
|
||||
@@ -104,6 +111,11 @@ export default {
|
||||
spec: { version: this.version }
|
||||
};
|
||||
|
||||
if (this.skipSingleReplicaDetachedVolFeatureEnabled && this.skipSingleReplicaDetachedVol) {
|
||||
upgradeValue.metadata.annotations =
|
||||
{ [HCI_ANNOTATIONS.SKIP_SINGLE_REPLICA_DETACHED_VOL]: JSON.stringify(this.skipSingleReplicaDetachedVol) };
|
||||
}
|
||||
|
||||
if (this.canEnableLogging) {
|
||||
upgradeValue.spec.logEnabled = this.enableLogging;
|
||||
}
|
||||
@@ -190,6 +202,21 @@ export default {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="skipSingleReplicaDetachedVolFeatureEnabled"
|
||||
class="mb-5"
|
||||
>
|
||||
<Checkbox
|
||||
v-model:value="skipSingleReplicaDetachedVol"
|
||||
class="check"
|
||||
type="checkbox"
|
||||
:label="t('harvester.upgradePage.skipSingleReplicaDetachedVol')"
|
||||
/>
|
||||
</div>
|
||||
<hr
|
||||
v-if="version"
|
||||
class="divider"
|
||||
/>
|
||||
<div v-if="version">
|
||||
<p
|
||||
v-clean-html="t('harvester.upgradePage.releaseTip', {url: releaseLink}, true)"
|
||||
|
||||
@@ -43,7 +43,7 @@ export default {
|
||||
{{ t('harvester.upgradePage.upgradeInfo.tip') }}
|
||||
</p>
|
||||
|
||||
<p class="mb-5">
|
||||
<p>
|
||||
{{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a
|
||||
:href="releaseVersion"
|
||||
target="_blank"
|
||||
|
||||
@@ -174,10 +174,6 @@ export default {
|
||||
return driver === LONGHORN_DRIVER || this.allowEmptySnapshotClassNameFeatureEnabled;
|
||||
},
|
||||
|
||||
isBackupVolumeSnapshotClassNameSelectable(driver) {
|
||||
return driver === LONGHORN_DRIVER;
|
||||
},
|
||||
|
||||
add() {
|
||||
this.configArr.push({
|
||||
key: '',
|
||||
@@ -247,7 +243,6 @@ export default {
|
||||
:options="getVolumeSnapshotOptions(driver.key)"
|
||||
:required="isBackupVolumeSnapshotRequired(driver.key)"
|
||||
:label="t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName')"
|
||||
@selectable="isBackupVolumeSnapshotClassNameSelectable(driver.key)"
|
||||
@keydown.native.enter.prevent="()=>{}"
|
||||
@update:value="update"
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script>
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
|
||||
import { Banner } from '@components/Banner';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
|
||||
@@ -62,7 +61,7 @@ export default {
|
||||
|
||||
<div
|
||||
class="row"
|
||||
@update:value="update"
|
||||
@input="update"
|
||||
>
|
||||
<div class="col span-12">
|
||||
<LabeledInput
|
||||
|
||||
@@ -12,6 +12,9 @@ import { NODE } from '@shell/config/types';
|
||||
import { HCI } from '../../types';
|
||||
import { DOC } from '../../config/doc-links';
|
||||
import { docLink } from '../../utils/feature-flags';
|
||||
import { NETWORK_TYPE } from '../../config/types';
|
||||
|
||||
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEditStorageNetwork',
|
||||
@@ -57,11 +60,14 @@ export default {
|
||||
data() {
|
||||
let parsedDefaultValue = {};
|
||||
let openVlan = false;
|
||||
let networkType = L2VLAN;
|
||||
|
||||
try {
|
||||
parsedDefaultValue = JSON.parse(this.value.value);
|
||||
networkType = 'vlan' in parsedDefaultValue ? L2VLAN : UNTAGGED; // backend doesn't provide networkType, so we check if vlan is provided instead
|
||||
openVlan = true;
|
||||
} catch (error) {
|
||||
networkType = L2VLAN;
|
||||
parsedDefaultValue = {
|
||||
vlan: '',
|
||||
clusterNetwork: '',
|
||||
@@ -73,6 +79,7 @@ export default {
|
||||
|
||||
return {
|
||||
openVlan,
|
||||
networkType,
|
||||
errors: [],
|
||||
exclude,
|
||||
parsedDefaultValue,
|
||||
@@ -87,16 +94,35 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
showVlan() {
|
||||
return this.networkType === L2VLAN;
|
||||
},
|
||||
networkTypes() {
|
||||
const types = [L2VLAN];
|
||||
|
||||
if (this.untaggedNetworkSettingEnabled) {
|
||||
types.push(UNTAGGED);
|
||||
}
|
||||
|
||||
return types;
|
||||
},
|
||||
storageNetworkExampleLink() {
|
||||
const version = this.$store.getters['harvester-common/getServerVersion']();
|
||||
|
||||
return docLink(DOC.STORAGE_NETWORK_EXAMPLE, version);
|
||||
},
|
||||
|
||||
untaggedNetworkSettingEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('untaggedNetworkSetting');
|
||||
},
|
||||
|
||||
clusterNetworkOptions() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||
// untaggedNetwork filter out mgmt cluster network
|
||||
const clusterNetworksOptions = this.networkType === UNTAGGED ? clusterNetworks.filter((net) => net.id !== 'mgmt') : clusterNetworks;
|
||||
|
||||
return clusterNetworks.map((n) => {
|
||||
return clusterNetworksOptions.map((n) => {
|
||||
const disabled = !n.isReadyForStorageNetwork;
|
||||
|
||||
return {
|
||||
@@ -108,7 +134,50 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
networkType: {
|
||||
handler(neu) {
|
||||
this.parsedDefaultValue.clusterNetwork = '';
|
||||
|
||||
if (neu === L2VLAN) {
|
||||
this.parsedDefaultValue.vlan = '';
|
||||
} else {
|
||||
delete this.parsedDefaultValue.vlan;
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
inputVlan(neu) {
|
||||
if (neu === '') {
|
||||
this.parsedDefaultValue.vlan = '';
|
||||
|
||||
return;
|
||||
}
|
||||
const newValue = Number(neu);
|
||||
|
||||
if (newValue > 4094) {
|
||||
this.parsedDefaultValue.vlan = 4094;
|
||||
} else if (newValue < 1) {
|
||||
this.parsedDefaultValue.vlan = 1;
|
||||
} else {
|
||||
this.parsedDefaultValue.vlan = newValue;
|
||||
}
|
||||
},
|
||||
|
||||
useDefault() {
|
||||
this.openVlan = false;
|
||||
this.networkType = L2VLAN;
|
||||
this.parsedDefaultValue = {
|
||||
vlan: '',
|
||||
clusterNetwork: '',
|
||||
range: '',
|
||||
exclude: []
|
||||
};
|
||||
},
|
||||
|
||||
update() {
|
||||
const exclude = this.exclude.filter((ip) => ip);
|
||||
|
||||
@@ -138,7 +207,7 @@ export default {
|
||||
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true));
|
||||
}
|
||||
|
||||
if (!this.parsedDefaultValue.vlan) {
|
||||
if (this.networkType === L2VLAN && !this.parsedDefaultValue.vlan) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
|
||||
}
|
||||
|
||||
@@ -191,13 +260,25 @@ export default {
|
||||
/>
|
||||
|
||||
<div v-if="openVlan">
|
||||
<LabeledSelect
|
||||
v-model:value="networkType"
|
||||
class="mb-20"
|
||||
:options="networkTypes"
|
||||
:mode="mode"
|
||||
:label="t('harvester.fields.type')"
|
||||
required
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-if="showVlan"
|
||||
v-model:value.number="parsedDefaultValue.vlan"
|
||||
type="number"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
required
|
||||
placeholder="e.g. 1 - 4094"
|
||||
label-key="harvester.setting.storageNetwork.vlan"
|
||||
@update:value="inputVlan"
|
||||
/>
|
||||
|
||||
<LabeledSelect
|
||||
|
||||
@@ -1,70 +1,55 @@
|
||||
// https://github.com/harvester/dashboard/releases/tag/v1.3.0
|
||||
const featuresV130 = [
|
||||
'supportHarvesterClusterVersion'
|
||||
];
|
||||
import semver from 'semver';
|
||||
|
||||
// https://github.com/harvester/dashboard/releases/tag/v1.3.1
|
||||
const featuresV131 = [
|
||||
...featuresV130,
|
||||
'autoRotateRke2CertsSetting',
|
||||
'supportBundleNodeCollectionTimeoutSetting'
|
||||
];
|
||||
|
||||
// https://github.com/harvester/dashboard/releases/tag/v1.3.2
|
||||
const featuresV132 = [
|
||||
...featuresV131,
|
||||
'kubeconfigDefaultTokenTTLMinutesSetting',
|
||||
'improveMaintenanceMode',
|
||||
];
|
||||
|
||||
// TODO: add v1.3.3 official release note
|
||||
// https://github.com/harvester/dashboard/releases/tag/v1.3.3-dev-20250105
|
||||
const featuresV133 = [
|
||||
...featuresV132,
|
||||
];
|
||||
|
||||
// https://github.com/harvester/dashboard/releases/tag/v1.4.0
|
||||
const featuresV140 = [
|
||||
...featuresV132,
|
||||
'cpuPinning',
|
||||
'usbPassthrough',
|
||||
'volumeEncryption',
|
||||
'schedulingVMBackup',
|
||||
'vmSnapshotQuota',
|
||||
'longhornV2LVMSupport',
|
||||
'improveMaintenanceMode',
|
||||
];
|
||||
|
||||
// https://github.com/harvester/dashboard/releases/tag/v1.4.1
|
||||
const featuresV141 = [
|
||||
...featuresV140
|
||||
];
|
||||
|
||||
// https://github.com/harvester/dashboard/releases/tag/v1.4.2
|
||||
const featuresV142 = [
|
||||
...featuresV141,
|
||||
'refreshIntervalInSecond',
|
||||
'allowEmptySnapshotClassName'
|
||||
];
|
||||
|
||||
// https://github.com/harvester/dashboard/releases/tag/v1.4.3-rc1
|
||||
const featuresV143 = [
|
||||
...featuresV142,
|
||||
];
|
||||
|
||||
// https://github.com/harvester/harvester-ui-extension/releases/tag/v1.5.0
|
||||
const featuresV150 = [
|
||||
...featuresV142
|
||||
];
|
||||
|
||||
export const RELEASE_FEATURES = {
|
||||
'v1.3.0': featuresV130,
|
||||
'v1.3.1': featuresV131,
|
||||
'v1.3.2': featuresV132,
|
||||
'v1.3.3': featuresV133,
|
||||
'v1.4.0': featuresV140,
|
||||
'v1.4.1': featuresV141,
|
||||
'v1.4.2': featuresV142,
|
||||
'v1.4.3': featuresV143,
|
||||
'v1.5.0': featuresV150
|
||||
const FEATURE_FLAGS = {
|
||||
'v1.3.0': [
|
||||
'supportHarvesterClusterVersion'
|
||||
],
|
||||
'v1.3.1': [
|
||||
'autoRotateRke2CertsSetting',
|
||||
'supportBundleNodeCollectionTimeoutSetting'
|
||||
],
|
||||
'v1.3.2': [
|
||||
'kubeconfigDefaultTokenTTLMinutesSetting',
|
||||
'improveMaintenanceMode',
|
||||
],
|
||||
'v1.3.3': [],
|
||||
'v1.4.0': [
|
||||
'cpuPinning',
|
||||
'usbPassthrough',
|
||||
'volumeEncryption',
|
||||
'schedulingVMBackup',
|
||||
'vmSnapshotQuota',
|
||||
'longhornV2LVMSupport',
|
||||
'improveMaintenanceMode',
|
||||
],
|
||||
'v1.4.1': [],
|
||||
'v1.4.2': [
|
||||
'refreshIntervalInSecond',
|
||||
'allowEmptySnapshotClassName'
|
||||
],
|
||||
'v1.4.3': [],
|
||||
'v1.5.0': [
|
||||
'tpmPersistentState',
|
||||
'efiPersistentState',
|
||||
'untaggedNetworkSetting',
|
||||
'skipSingleReplicaDetachedVol',
|
||||
'thirdPartyStorage'
|
||||
],
|
||||
'v1.5.1': [],
|
||||
};
|
||||
|
||||
const generateFeatureFlags = () => {
|
||||
const versions = [...Object.keys(FEATURE_FLAGS)].filter((version) => semver.valid(version)).sort(semver.compare);
|
||||
|
||||
const generatedFlags = {};
|
||||
|
||||
versions.forEach((version, index) => {
|
||||
const previousVersion = versions[index - 1];
|
||||
|
||||
generatedFlags[version] = previousVersion ? [...generatedFlags[previousVersion], ...FEATURE_FLAGS[version]] : [...FEATURE_FLAGS[version]];
|
||||
});
|
||||
|
||||
return generatedFlags;
|
||||
};
|
||||
|
||||
export const RELEASE_FEATURES = generateFeatureFlags();
|
||||
|
||||
@@ -31,7 +31,8 @@ import {
|
||||
FINGERPRINT,
|
||||
IMAGE_PROGRESS,
|
||||
SNAPSHOT_TARGET_VOLUME,
|
||||
IMAGE_VIRTUAL_SIZE
|
||||
IMAGE_VIRTUAL_SIZE,
|
||||
IMAGE_STORAGE_CLASS
|
||||
} from './table-headers';
|
||||
|
||||
const TEMPLATE = HCI.VM_VERSION;
|
||||
@@ -221,6 +222,7 @@ export function init($plugin, store) {
|
||||
STATE,
|
||||
NAME_COL,
|
||||
NAMESPACE_COL,
|
||||
IMAGE_STORAGE_CLASS,
|
||||
IMAGE_PROGRESS,
|
||||
IMAGE_DOWNLOAD_SIZE,
|
||||
IMAGE_VIRTUAL_SIZE,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// TODO: delete this not used variable
|
||||
export const MemoryUnit = [{
|
||||
label: 'Mi',
|
||||
value: 'Mi'
|
||||
|
||||
@@ -7,60 +7,64 @@ export const LABELS_TO_IGNORE_REGEX = [
|
||||
];
|
||||
|
||||
export const HCI = {
|
||||
CLOUD_INIT: 'harvesterhci.io/cloud-init-template',
|
||||
CURRENT_IP: 'rke2.io/internal-ip',
|
||||
IMAGE_ID: 'harvesterhci.io/imageId',
|
||||
SSH_NAMES: 'harvesterhci.io/sshNames',
|
||||
NETWORK_IPS: 'network.harvesterhci.io/ips',
|
||||
TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName',
|
||||
CREATOR: 'harvesterhci.io/creator',
|
||||
OS: 'harvesterhci.io/os',
|
||||
NETWORK_TYPE: 'network.harvesterhci.io/type',
|
||||
VM_NAME: 'harvesterhci.io/vmName',
|
||||
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
|
||||
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
|
||||
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
|
||||
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
|
||||
HOST_CONSOLE_URL: 'harvesterhci.io/host-console-url',
|
||||
RESTORE_NAME: 'restore.harvesterhci.io/name',
|
||||
NODE_ROLE_MASTER: 'node-role.kubernetes.io/master',
|
||||
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
|
||||
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
|
||||
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
|
||||
MIGRATION_STATE: 'harvesterhci.io/migrationState',
|
||||
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
|
||||
IMAGE_NAME: 'harvesterhci.io/image-name',
|
||||
INIT_IP: 'etcd.rke2.cattle.io/node-address',
|
||||
NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
|
||||
NETWORK_ROUTE: 'network.harvesterhci.io/route',
|
||||
MATCHED_NODES: 'network.harvesterhci.io/matched-nodes',
|
||||
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
|
||||
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
|
||||
UPGRADE_STATE: 'harvesterhci.io/upgradeState',
|
||||
REAY_MESSAGE: 'harvesterhci.io/read-message',
|
||||
DYNAMIC_SSHKEYS_NAMES: 'harvesterhci.io/dynamic-ssh-key-names',
|
||||
DYNAMIC_SSHKEYS_USERS: 'harvesterhci.io/dynamic-ssh-key-users',
|
||||
IMAGE_SUFFIX: 'harvesterhci.io/image-type',
|
||||
OS_TYPE: 'harvesterhci.io/os-type',
|
||||
STORAGE_PROVISIONER: 'harvesterhci.io/storageProvisioner',
|
||||
HOST_REQUEST: 'management.cattle.io/pod-requests',
|
||||
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
|
||||
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
|
||||
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
|
||||
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
|
||||
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
|
||||
KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx',
|
||||
NODE_MANUFACTURER: 'manufacturer',
|
||||
NODE_MODEL: 'model',
|
||||
NODE_SERIAL_NUMBER: 'serialNumber',
|
||||
VM_INSUFFICIENT: 'harvesterhci.io/insufficient-resource-quota',
|
||||
NODE_NTP_SYNC_STATUS: 'node.harvesterhci.io/ntp-service',
|
||||
PARENT_SRIOV: 'harvesterhci.io/parent-sriov-network-device',
|
||||
PARENT_SRIOV_GPU: 'harvesterhci.io/parentSRIOVGPUDevice',
|
||||
VM_MAINTENANCE_MODE_STRATEGY: 'harvesterhci.io/maintain-mode-strategy',
|
||||
NODE_CPU_MANAGER_UPDATE_STATUS: 'harvesterhci.io/cpu-manager-update-status',
|
||||
CPU_MANAGER: 'cpumanager',
|
||||
VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails',
|
||||
SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId',
|
||||
DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine',
|
||||
CLOUD_INIT: 'harvesterhci.io/cloud-init-template',
|
||||
CURRENT_IP: 'rke2.io/internal-ip',
|
||||
IMAGE_ID: 'harvesterhci.io/imageId',
|
||||
SSH_NAMES: 'harvesterhci.io/sshNames',
|
||||
NETWORK_IPS: 'network.harvesterhci.io/ips',
|
||||
TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName',
|
||||
CREATOR: 'harvesterhci.io/creator',
|
||||
OS: 'harvesterhci.io/os',
|
||||
GOLDEN_IMAGE: 'harvesterhci.io/goldenImage',
|
||||
NETWORK_TYPE: 'network.harvesterhci.io/type',
|
||||
VM_NAME: 'harvesterhci.io/vmName',
|
||||
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
|
||||
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
|
||||
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
|
||||
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
|
||||
HOST_CONSOLE_URL: 'harvesterhci.io/host-console-url',
|
||||
RESTORE_NAME: 'restore.harvesterhci.io/name',
|
||||
NODE_ROLE_MASTER: 'node-role.kubernetes.io/master',
|
||||
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
|
||||
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
|
||||
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
|
||||
MIGRATION_STATE: 'harvesterhci.io/migrationState',
|
||||
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
|
||||
IMAGE_NAME: 'harvesterhci.io/image-name',
|
||||
INIT_IP: 'etcd.rke2.cattle.io/node-address',
|
||||
NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
|
||||
NETWORK_ROUTE: 'network.harvesterhci.io/route',
|
||||
MATCHED_NODES: 'network.harvesterhci.io/matched-nodes',
|
||||
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
|
||||
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
|
||||
UPGRADE_STATE: 'harvesterhci.io/upgradeState',
|
||||
REAY_MESSAGE: 'harvesterhci.io/read-message',
|
||||
DYNAMIC_SSHKEYS_NAMES: 'harvesterhci.io/dynamic-ssh-key-names',
|
||||
DYNAMIC_SSHKEYS_USERS: 'harvesterhci.io/dynamic-ssh-key-users',
|
||||
IMAGE_SUFFIX: 'harvesterhci.io/image-type',
|
||||
OS_TYPE: 'harvesterhci.io/os-type',
|
||||
STORAGE_PROVISIONER: 'harvesterhci.io/storageProvisioner',
|
||||
SKIP_SINGLE_REPLICA_DETACHED_VOL: 'harvesterhci.io/skipSingleReplicaDetachedVol',
|
||||
HOST_REQUEST: 'management.cattle.io/pod-requests',
|
||||
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
|
||||
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
|
||||
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
|
||||
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
|
||||
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
|
||||
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
|
||||
KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx',
|
||||
NODE_MANUFACTURER: 'manufacturer',
|
||||
NODE_MODEL: 'model',
|
||||
NODE_SERIAL_NUMBER: 'serialNumber',
|
||||
VM_INSUFFICIENT: 'harvesterhci.io/insufficient-resource-quota',
|
||||
NODE_NTP_SYNC_STATUS: 'node.harvesterhci.io/ntp-service',
|
||||
PARENT_SRIOV: 'harvesterhci.io/parent-sriov-network-device',
|
||||
PARENT_SRIOV_GPU: 'harvesterhci.io/parentSRIOVGPUDevice',
|
||||
VM_MAINTENANCE_MODE_STRATEGY: 'harvesterhci.io/maintain-mode-strategy',
|
||||
NODE_CPU_MANAGER_UPDATE_STATUS: 'harvesterhci.io/cpu-manager-update-status',
|
||||
CPU_MANAGER: 'cpumanager',
|
||||
VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails',
|
||||
SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId',
|
||||
DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine',
|
||||
K8S_ARCH: 'kubernetes.io/arch'
|
||||
};
|
||||
|
||||
@@ -65,7 +65,9 @@ export const HCI_ALLOWED_SETTINGS = {
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: {},
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.STORAGE_NETWORK]: { kind: 'custom', from: 'import' },
|
||||
[HCI_SETTING.STORAGE_NETWORK]: {
|
||||
kind: 'custom', from: 'import', canReset: true
|
||||
},
|
||||
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.SSL_PARAMETERS]: {
|
||||
|
||||
@@ -88,3 +88,13 @@ export const MACHINE_POOLS = {
|
||||
align: 'center',
|
||||
width: 100,
|
||||
};
|
||||
|
||||
// The STORAGE_CLASS column in VM image list page
|
||||
export const IMAGE_STORAGE_CLASS = {
|
||||
name: 'imageStorageClass',
|
||||
labelKey: 'harvester.tableHeaders.storageClass',
|
||||
sort: 'imageStorageClass',
|
||||
value: 'imageStorageClass',
|
||||
align: 'left',
|
||||
width: 200,
|
||||
};
|
||||
|
||||
@@ -2,3 +2,13 @@ export const BACKUP_TYPE = {
|
||||
BACKUP: 'backup',
|
||||
SNAPSHOT: 'snapshot'
|
||||
};
|
||||
|
||||
export const NETWORK_TYPE = {
|
||||
L2VLAN: 'L2VlanNetwork',
|
||||
UNTAGGED: 'UntaggedNetwork'
|
||||
};
|
||||
|
||||
export const VOLUME_MODE = {
|
||||
BLOCK: 'Block',
|
||||
FILE_SYSTEM: 'Filesystem'
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import { LONGHORN, METRIC } from '@shell/config/types';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { UNIT_SUFFIX } from '../../utils/unit';
|
||||
import HarvesterCPUUsed from '../../formatters/HarvesterCPUUsed';
|
||||
import HarvesterMemoryUsed from '../../formatters/HarvesterMemoryUsed';
|
||||
import HarvesterStorageUsed from '../../formatters/HarvesterStorageUsed';
|
||||
@@ -121,7 +122,7 @@ export default {
|
||||
memoryUnits() {
|
||||
const exponent = exponentNeeded(this.memoryTotal, 1024);
|
||||
|
||||
return `${ UNITS[exponent] }iB`;
|
||||
return `${ UNITS[exponent] }${ UNIT_SUFFIX }`;
|
||||
},
|
||||
|
||||
nodeType() {
|
||||
|
||||
@@ -9,7 +9,7 @@ import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
|
||||
import Loading from '@shell/components/Loading.vue';
|
||||
import SortableTable from '@shell/components/SortableTable';
|
||||
import Banner from '@components/Banner/Banner.vue';
|
||||
|
||||
import { UNIT_SUFFIX } from '../../utils/unit';
|
||||
import metricPoller from '@shell/mixins/metric-poller';
|
||||
import {
|
||||
METRIC, NODE, LONGHORN, POD, EVENT
|
||||
@@ -178,7 +178,7 @@ export default {
|
||||
minExponent: 3,
|
||||
maxExponent: 3,
|
||||
maxPrecision: 2,
|
||||
suffix: 'iB',
|
||||
suffix: UNIT_SUFFIX,
|
||||
};
|
||||
|
||||
const longhornDisks = Object.keys(diskStatus).map((key) => {
|
||||
|
||||
@@ -106,6 +106,12 @@ export default {
|
||||
:label="t('harvester.modal.cloneVM.name')"
|
||||
required
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template
|
||||
@@ -127,11 +133,6 @@ export default {
|
||||
@click="create"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
/>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { alternateLabel } from '@shell/utils/platform';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { Card } from '@components/Card';
|
||||
import { escapeHtml } from '@shell/utils/string';
|
||||
|
||||
/**
|
||||
* @name ConfirmExecutionDialog
|
||||
* @description Dialog component to confirm the related resources before executing the action.
|
||||
*/
|
||||
export default {
|
||||
name: 'ConfirmExecutionDialog',
|
||||
|
||||
emits: ['close'],
|
||||
|
||||
components: {
|
||||
AsyncButton,
|
||||
Banner,
|
||||
Card,
|
||||
},
|
||||
|
||||
props: {
|
||||
/**
|
||||
* @property resources to be deleted.
|
||||
* @type {Resource[]} Array of the resource model's instance
|
||||
*/
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return { errors: [] };
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState('action-menu', ['modalData']),
|
||||
|
||||
warningMessageKey() {
|
||||
return this.modalData.warningMessageKey;
|
||||
},
|
||||
|
||||
names() {
|
||||
return this.resources.map((obj) => obj.nameDisplay).slice(0, 5);
|
||||
},
|
||||
|
||||
resourceNames() {
|
||||
return this.names.reduce((res, name, i) => {
|
||||
if (i >= 5) {
|
||||
return res;
|
||||
}
|
||||
res += `<b>${ escapeHtml(name) }</b>`;
|
||||
if (i === this.names.length - 1) {
|
||||
res += this.plusMore;
|
||||
} else {
|
||||
res += i === this.resources.length - 2 ? ' and ' : ', ';
|
||||
}
|
||||
|
||||
return res;
|
||||
}, '');
|
||||
},
|
||||
|
||||
plusMore() {
|
||||
const remaining = this.resources.length - this.names.length;
|
||||
|
||||
return this.t('dialog.confirmExecution.andOthers', { count: remaining }, true);
|
||||
},
|
||||
|
||||
type() {
|
||||
const types = new Set(this.resources.reduce((array, each) => {
|
||||
array.push(each.type);
|
||||
|
||||
return array;
|
||||
}, []));
|
||||
|
||||
if (types.size > 1) {
|
||||
return this.t('generic.resource', { count: this.resources.length });
|
||||
}
|
||||
|
||||
const schema = this.resources[0]?.schema;
|
||||
|
||||
if ( !schema ) {
|
||||
return `resource${ this.resources.length === 1 ? '' : 's' }`;
|
||||
}
|
||||
|
||||
return this.$store.getters['type-map/labelFor'](schema, this.resources.length);
|
||||
},
|
||||
|
||||
protip() {
|
||||
return this.t('dialog.confirmExecution.protip', { alternateLabel });
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
escapeHtml,
|
||||
|
||||
close() {
|
||||
this.errors = [];
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async apply(buttonDone) {
|
||||
try {
|
||||
for (const resource of this.resources) {
|
||||
await resource.doActionGrowl(this.modalData.action, {});
|
||||
}
|
||||
buttonDone(true);
|
||||
this.close();
|
||||
} catch (e) {
|
||||
this.errors = exceptionToErrorsArray(e);
|
||||
buttonDone(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
<h4 class="text-default-text">
|
||||
{{ t('dialog.confirmExecution.title') }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="pl-10 pr-10">
|
||||
<span
|
||||
v-clean-html="t(warningMessageKey, { type, names: resourceNames }, true)"
|
||||
></span>
|
||||
<div class="text-info mt-20">
|
||||
{{ protip }}
|
||||
</div>
|
||||
<Banner
|
||||
v-for="(error, i) in errors"
|
||||
:key="i"
|
||||
:label="error"
|
||||
color="error"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="actions">
|
||||
<button
|
||||
class="btn role-secondary"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
<AsyncButton
|
||||
mode="apply"
|
||||
class="btn bg-primary ml-10"
|
||||
:disabled="applyDisabled"
|
||||
@click="apply"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.modal-container {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -42,8 +42,25 @@ export default {
|
||||
computed: {
|
||||
...mapState('action-menu', ['modalData']),
|
||||
|
||||
warningMessageKey() {
|
||||
return this.modalData.warningMessageKey;
|
||||
title() {
|
||||
return this.modalData.title || 'dialog.promptRemove.title';
|
||||
},
|
||||
|
||||
formattedType() {
|
||||
return this.type.toLowerCase();
|
||||
},
|
||||
|
||||
warningMessage() {
|
||||
if (this.modalData.warningMessage) return this.modalData.warningMessage;
|
||||
|
||||
const isPlural = this.type.endsWith('s');
|
||||
const thisOrThese = isPlural ? 'these' : 'this';
|
||||
const defaultMessage = this.t('dialog.promptRemove.warningMessage', {
|
||||
type: this.formattedType,
|
||||
thisOrThese,
|
||||
});
|
||||
|
||||
return defaultMessage;
|
||||
},
|
||||
|
||||
names() {
|
||||
@@ -70,6 +87,12 @@ export default {
|
||||
return this.resources[0].nameDisplay;
|
||||
},
|
||||
|
||||
needConfirmation() {
|
||||
const { needConfirmation = true } = this.modalData ;
|
||||
|
||||
return needConfirmation === true;
|
||||
},
|
||||
|
||||
plusMore() {
|
||||
const remaining = this.resources.length - this.names.length;
|
||||
|
||||
@@ -97,11 +120,15 @@ export default {
|
||||
},
|
||||
|
||||
deleteDisabled() {
|
||||
if (!this.needConfirmation) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.confirmName !== this.nameToMatch;
|
||||
},
|
||||
|
||||
protip() {
|
||||
return this.t('promptRemove.protip', { alternateLabel });
|
||||
return this.t('dialog.promptRemove.protip', { alternateLabel });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -137,31 +164,39 @@ export default {
|
||||
>
|
||||
<template #title>
|
||||
<h4 class="text-default-text">
|
||||
{{ t('promptRemove.title') }}
|
||||
{{ t(title, { type }, true) }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="pl-10 pr-10">
|
||||
<span
|
||||
v-clean-html="t(warningMessageKey, { type, names: resourceNames }, true)"
|
||||
v-clean-html="warningMessage"
|
||||
></span>
|
||||
|
||||
<div class="mt-10 mb-10">
|
||||
<span
|
||||
v-clean-html="t('promptRemove.confirmName', { nameToMatch: escapeHtml(nameToMatch) }, true)"
|
||||
></span>
|
||||
</div>
|
||||
<div class="mb-10">
|
||||
<CopyToClipboardText :text="nameToMatch" />
|
||||
</div>
|
||||
<input
|
||||
id="confirm"
|
||||
v-model="confirmName"
|
||||
type="text"
|
||||
/>
|
||||
<div class="text-info mt-20">
|
||||
{{ protip }}
|
||||
<div
|
||||
v-if="needConfirmation"
|
||||
class="mt-20"
|
||||
>
|
||||
<div class="mt-10 mb-10">
|
||||
<span
|
||||
v-clean-html="t('dialog.promptRemove.confirmName', {
|
||||
type: formattedType,
|
||||
nameToMatch: escapeHtml(nameToMatch)
|
||||
}, true)"
|
||||
></span>
|
||||
</div>
|
||||
<div class="mb-10">
|
||||
<CopyToClipboardText :text="nameToMatch" />
|
||||
</div>
|
||||
<input
|
||||
id="confirm"
|
||||
v-model="confirmName"
|
||||
type="text"
|
||||
/>
|
||||
<div class="text-info mt-20">
|
||||
{{ protip }}
|
||||
</div>
|
||||
</div>
|
||||
<Banner
|
||||
v-for="(error, i) in errors"
|
||||
|
||||
@@ -57,6 +57,10 @@ export default {
|
||||
if (!!pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
|
||||
return false;
|
||||
}
|
||||
// we won't show golden image volume in the hot plug volume modal
|
||||
if (pvc.isGoldenImageVolume) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !pvc.attachVM;
|
||||
})
|
||||
|
||||
@@ -34,9 +34,14 @@ export default {
|
||||
|
||||
await allHash(hash);
|
||||
|
||||
const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find((s) => s.isDefault);
|
||||
const allStorages = this.allStorageClasses;
|
||||
const defaultStorage = allStorages.find((s) => s.isDefault);
|
||||
|
||||
this['storageClassName'] = defaultStorage?.metadata?.name || 'longhorn';
|
||||
if (this.isLonghornV1Volume) {
|
||||
this['storageClassName'] = defaultStorage?.metadata?.name || 'longhorn';
|
||||
} else {
|
||||
this['storageClassName'] = this.nonLonghornV1StorageClasses[0]?.metadata?.name || '';
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
@@ -54,7 +59,7 @@ export default {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
return this.resources[0] || {};
|
||||
},
|
||||
|
||||
namespaces() {
|
||||
@@ -73,15 +78,33 @@ export default {
|
||||
return out;
|
||||
},
|
||||
|
||||
isLonghornV1Volume() {
|
||||
return this.actionResource?.isLonghornV1 === true;
|
||||
},
|
||||
|
||||
disableSave() {
|
||||
return !(this.name && this.namespace && this.storageClassName);
|
||||
},
|
||||
|
||||
storageClassOptions() {
|
||||
allStorageClasses() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
||||
|
||||
const out = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||
return this.$store.getters[`${ inStore }/all`](STORAGE_CLASS) || [];
|
||||
},
|
||||
|
||||
nonLonghornV1StorageClasses() {
|
||||
return this.allStorageClasses.filter((s) => s.isLonghornV1 === false) || [];
|
||||
},
|
||||
|
||||
storageClassOptions() {
|
||||
let storages = this.allStorageClasses;
|
||||
|
||||
// Volume with non-longhorn v1 sc can't be exported to longhorn v1 sc image
|
||||
if (!this.isLonghornV1Volume) {
|
||||
storages = this.allStorageClasses.filter((s) => s.isLonghornV1 === false);
|
||||
}
|
||||
|
||||
const options = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
|
||||
return {
|
||||
@@ -90,7 +113,7 @@ export default {
|
||||
};
|
||||
}) || [];
|
||||
|
||||
return out;
|
||||
return options;
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { HCI } from '../types';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import AppModal from '@shell/components/AppModal';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterImageDownloaderDialog',
|
||||
|
||||
emits: ['close'],
|
||||
|
||||
components: {
|
||||
AsyncButton, Banner, Card, AppModal
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return { errors: [], isOpen: false };
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
downloadImageInProgress() {
|
||||
return this.$store.getters['harvester-common/isDownloadImageInProgress'];
|
||||
},
|
||||
|
||||
image() {
|
||||
return this.resources[0] || {};
|
||||
},
|
||||
|
||||
imageName() {
|
||||
return this.image?.name || '';
|
||||
},
|
||||
|
||||
imageVirtualSize() {
|
||||
return this.image?.virtualSize || this.image?.downSize || '';
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async cancelDownload() {
|
||||
const url = this.image?.links?.downloadcancel;
|
||||
|
||||
if (url) {
|
||||
await this.$store.dispatch('harvester/request', { url });
|
||||
}
|
||||
},
|
||||
|
||||
async close() {
|
||||
if (this.downloadImageInProgress) {
|
||||
this.$store.commit('harvester-common/setDownloadImageCancel', true);
|
||||
this.$store.commit('harvester-common/setDownloadImageInProgress', false);
|
||||
await this.cancelDownload();
|
||||
}
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async startDownload(buttonCb) {
|
||||
// clean the download image CRD first.
|
||||
await this.cancelDownload();
|
||||
this.$store.commit('harvester-common/setDownloadImageCancel', false);
|
||||
this.$store.commit('harvester-common/setDownloadImageInProgress', false);
|
||||
this.errors = [];
|
||||
|
||||
const name = this.image?.name || '';
|
||||
const namespace = this.image?.namespace || '';
|
||||
|
||||
const imageCrd = {
|
||||
apiVersion: 'harvesterhci.io/v1beta1',
|
||||
type: HCI.VM_IMAGE_DOWNLOADER,
|
||||
kind: 'VirtualMachineImageDownloader',
|
||||
metadata: {
|
||||
name,
|
||||
namespace
|
||||
},
|
||||
spec: { imageName: name }
|
||||
};
|
||||
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const imageCreate = await this.$store.dispatch(`${ inStore }/create`, imageCrd);
|
||||
|
||||
try {
|
||||
await imageCreate.save();
|
||||
this.$store.commit('harvester-common/setDownloadImageId', `${ namespace }/${ name }`, { root: true });
|
||||
this.$store.dispatch('harvester-common/downloadImageProgress', { root: true });
|
||||
} catch (err) {
|
||||
this.errors = exceptionToErrorsArray(err);
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<app-modal
|
||||
class="image-downloader-modal"
|
||||
name="image-download-dialog"
|
||||
height="auto"
|
||||
:width="600"
|
||||
:click-to-close="false"
|
||||
@close="close"
|
||||
>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
{{ t('harvester.modal.downloadImage.title') }}
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<Banner color="info">
|
||||
{{ t('harvester.modal.downloadImage.banner', { size: imageVirtualSize }) }}
|
||||
</Banner>
|
||||
{{ t('harvester.modal.downloadImage.startMessage') }}
|
||||
<br /><br />
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="actions">
|
||||
<div class="buttons">
|
||||
<button
|
||||
class="btn role-secondary mr-10"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
<AsyncButton
|
||||
type="submit"
|
||||
mode="download"
|
||||
:disabled="downloadImageInProgress"
|
||||
@click="startDownload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</app-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,7 @@ import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { GIBIBYTE } from '../utils/unit';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterVMQuotaDialog',
|
||||
@@ -29,6 +30,7 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
GIBIBYTE,
|
||||
totalSnapshotSize: '',
|
||||
errors: []
|
||||
};
|
||||
@@ -44,7 +46,6 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
close() {
|
||||
this.totalSnapshotSize = '';
|
||||
this.$emit('close');
|
||||
@@ -92,7 +93,7 @@ export default {
|
||||
:input-exponent="3"
|
||||
:increment="1024"
|
||||
:output-modifier="true"
|
||||
suffix="GiB"
|
||||
:suffix="GIBIBYTE"
|
||||
class="mb-20"
|
||||
/>
|
||||
<Banner
|
||||
|
||||
@@ -20,7 +20,7 @@ import Loading from '@shell/components/Loading.vue';
|
||||
import MessageLink from '@shell/components/MessageLink';
|
||||
import { ADD_ONS } from '@pkg/harvester/config/harvester-map';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester';
|
||||
|
||||
import { UNIT_SUFFIX } from '../../utils/unit';
|
||||
import { _EDIT } from '@shell/config/query-params';
|
||||
import { sortBy } from '@shell/utils/sort';
|
||||
import { Banner } from '@components/Banner';
|
||||
@@ -185,7 +185,7 @@ export default {
|
||||
minExponent: 3,
|
||||
maxExponent: 3,
|
||||
maxPrecision: 2,
|
||||
suffix: 'iB',
|
||||
suffix: UNIT_SUFFIX,
|
||||
};
|
||||
|
||||
const longhornDisks = Object.keys(diskStatus).map((key) => {
|
||||
@@ -457,7 +457,7 @@ export default {
|
||||
const devPath = d.spec?.devPath;
|
||||
const deviceType = d.status?.deviceStatus?.details?.deviceType;
|
||||
const sizeBytes = d.status?.deviceStatus?.capacity?.sizeBytes;
|
||||
const size = formatSi(sizeBytes, { increment: 1024 });
|
||||
const size = formatSi(sizeBytes, { increment: 1024, suffix: UNIT_SUFFIX });
|
||||
const parentDevice = d.status?.deviceStatus?.parentDevice;
|
||||
const isChildAdded = this.newDisks.find((newDisk) => newDisk.blockDevice?.status?.deviceStatus?.parentDevice === devPath);
|
||||
const name = d.displayName;
|
||||
|
||||
@@ -11,6 +11,9 @@ import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-anno
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { HCI } from '../types';
|
||||
import { NETWORK_TYPE } from '../config/types';
|
||||
|
||||
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
|
||||
|
||||
const AUTO = 'auto';
|
||||
const MANUAL = 'manual';
|
||||
@@ -48,7 +51,7 @@ export default {
|
||||
config.bridge = config.bridge.slice(0, -3);
|
||||
}
|
||||
|
||||
const type = this.value.vlanType || 'L2VlanNetwork' ;
|
||||
const type = this.value.vlanType || L2VLAN ;
|
||||
|
||||
return {
|
||||
config,
|
||||
@@ -101,15 +104,15 @@ export default {
|
||||
},
|
||||
|
||||
networkType() {
|
||||
return ['L2VlanNetwork', 'UntaggedNetwork'];
|
||||
return [L2VLAN, UNTAGGED];
|
||||
},
|
||||
|
||||
isUntaggedNetwork() {
|
||||
if (this.isView) {
|
||||
return this.value.vlanType === 'UntaggedNetwork';
|
||||
return this.value.vlanType === UNTAGGED;
|
||||
}
|
||||
|
||||
return this.type === 'UntaggedNetwork';
|
||||
return this.type === UNTAGGED;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -23,10 +23,9 @@ import Tags from '../../components/DiskTags';
|
||||
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
|
||||
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
||||
|
||||
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
|
||||
|
||||
export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node';
|
||||
|
||||
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
|
||||
const VOLUME_BINDING_MODE_IMMEDIATE = 'Immediate';
|
||||
const VOLUME_BINDING_MODE_WAIT = 'WaitForFirstConsumer';
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ export default {
|
||||
diskSelector: null,
|
||||
nodeSelector: null,
|
||||
encrypted: 'false',
|
||||
migratable: 'false',
|
||||
migratable: this.value.thirdPartyStorageFeatureEnabled ? 'true' : 'false',
|
||||
dataEngine: DATA_ENGINE_V2
|
||||
};
|
||||
}
|
||||
@@ -318,7 +318,7 @@ export default {
|
||||
:label="t('harvester.storage.parameters.migratable.label')"
|
||||
:mode="mode"
|
||||
:options="migratableOptions"
|
||||
:disabled="true"
|
||||
:disabled="!value.thirdPartyStorageFeatureEnabled"
|
||||
/>
|
||||
</div>
|
||||
<div class="row mt-20">
|
||||
|
||||
@@ -24,6 +24,7 @@ const CLONE = 'clone';
|
||||
const DOWNLOAD = 'download';
|
||||
const UPLOAD = 'upload';
|
||||
const rawORqcow2 = 'raw_qcow2';
|
||||
const LONGHORN = 'longhorn';
|
||||
|
||||
export default {
|
||||
name: 'EditImage',
|
||||
@@ -61,11 +62,11 @@ export default {
|
||||
storageClasses: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
||||
});
|
||||
|
||||
const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find((s) => s.isDefault);
|
||||
|
||||
this['storageClassName'] = this.storageClassName || defaultStorage?.metadata?.name || 'longhorn';
|
||||
this['storageClassName'] = this.storageClassName || this.defaultStorageClassName();
|
||||
this.images = this.$store.getters[`${ inStore }/all`](HCI.IMAGE);
|
||||
|
||||
this.storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
||||
|
||||
const { securityParameters } = this.value.spec;
|
||||
|
||||
// edit and view mode should show the source image
|
||||
@@ -100,14 +101,15 @@ export default {
|
||||
}
|
||||
|
||||
return {
|
||||
selectedImage: null,
|
||||
images: [],
|
||||
url: this.value.spec.url,
|
||||
files: [],
|
||||
resource: '',
|
||||
headers: {},
|
||||
fileUrl: '',
|
||||
file: '',
|
||||
selectedImage: null,
|
||||
storageClasses: [],
|
||||
images: [],
|
||||
url: this.value.spec.url,
|
||||
files: [],
|
||||
resource: '',
|
||||
headers: {},
|
||||
fileUrl: '',
|
||||
file: '',
|
||||
};
|
||||
},
|
||||
|
||||
@@ -144,12 +146,26 @@ export default {
|
||||
];
|
||||
},
|
||||
|
||||
storageClassOptions() {
|
||||
encryptedStorageClasses() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
||||
|
||||
return storages
|
||||
.filter((s) => !s.parameters?.backingImage && s.provisioner !== LVM_DRIVER) // Lvm storage is not supported.
|
||||
return storages.filter((s) => s.isEncrypted);
|
||||
},
|
||||
|
||||
nonEncryptedStorageClasses() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
||||
|
||||
return storages.filter((s) => !s.isEncrypted);
|
||||
},
|
||||
|
||||
storageClassOptions() {
|
||||
const storages = this.value.spec?.securityParameters?.cryptoOperation === ENCRYPT ? this.encryptedStorageClasses : this.nonEncryptedStorageClasses;
|
||||
const filteredStorages = this.value.thirdPartyStorageFeatureEnabled ? storages.filter((s) => !s.parameters?.backingImage) : storages
|
||||
.filter((s) => !s.parameters?.backingImage && s.provisioner !== LVM_DRIVER) ;
|
||||
|
||||
return filteredStorages
|
||||
.map((s) => {
|
||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
|
||||
@@ -167,6 +183,9 @@ export default {
|
||||
|
||||
set(nue) {
|
||||
this.value.metadata.annotations[HCI_ANNOTATIONS.STORAGE_CLASS] = nue;
|
||||
if (this.value.thirdPartyStorageFeatureEnabled) {
|
||||
this.value.spec.targetStorageClassName = nue;
|
||||
}
|
||||
}
|
||||
},
|
||||
sourceImageOptions() {
|
||||
@@ -239,6 +258,20 @@ export default {
|
||||
this.$refs.file.value = null;
|
||||
}
|
||||
},
|
||||
'value.spec.securityParameters.cryptoOperation'() {
|
||||
if (this.value.spec?.securityParameters?.cryptoOperation === ENCRYPT) {
|
||||
this.storageClassName = this.encryptedStorageClasses[0]?.name || '';
|
||||
} else { // URL / FILE / DECRYPT should use default storage class
|
||||
this.storageClassName = this.defaultStorageClassName();
|
||||
}
|
||||
},
|
||||
'storageClassName'(neu) {
|
||||
const storageClass = this.storages.find((s) => s.id === neu);
|
||||
|
||||
if (storageClass && this.value.thirdPartyStorageFeatureEnabled) {
|
||||
this.value.spec.backend = storageClass.isLonghornV1 ? 'backingimage' : 'cdi';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -385,6 +418,18 @@ export default {
|
||||
return str.toLowerCase().includes(os.value.toLowerCase()) ? os.value : false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
defaultStorageClassName() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find((s) => s.isDefault);
|
||||
|
||||
if (!defaultStorage) {
|
||||
return LONGHORN;
|
||||
}
|
||||
|
||||
// if default sc is encrypted, use longhorn as default
|
||||
return defaultStorage.isEncrypted ? LONGHORN : defaultStorage?.metadata?.name;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,12 +6,11 @@ import { Checkbox } from '@components/Form/Checkbox';
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import Labels from '@shell/components/form/Labels';
|
||||
import { Banner } from '@components/Banner';
|
||||
import KeyValue from '@shell/components/form/KeyValue';
|
||||
import NodeScheduling from '@shell/components/form/NodeScheduling';
|
||||
import PodAffinity from '@shell/components/form/PodAffinity';
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
|
||||
import { randomStr } from '@shell/utils/string';
|
||||
import { _CONFIG, _EDIT, _VIEW } from '@shell/config/query-params';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
@@ -47,7 +46,7 @@ export default {
|
||||
PodAffinity,
|
||||
Reserved,
|
||||
UnitInput,
|
||||
Labels,
|
||||
Banner,
|
||||
KeyValue,
|
||||
},
|
||||
|
||||
@@ -321,34 +320,44 @@ export default {
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
:name="t('generic.labels')"
|
||||
:label="t('harvester.tab.instanceLabel')"
|
||||
:weight="-5"
|
||||
name="labels"
|
||||
:label="t('generic.labels')"
|
||||
:weight="-9"
|
||||
>
|
||||
<Labels
|
||||
:default-container-class="'labels-and-annotations-container'"
|
||||
:value="value"
|
||||
<Banner color="info">
|
||||
<t k="harvester.virtualMachine.labels.banner" />
|
||||
</Banner>
|
||||
<KeyValue
|
||||
key="labels"
|
||||
:value="value.labels"
|
||||
:add-label="t('labels.addLabel')"
|
||||
:mode="mode"
|
||||
:display-side-by-side="false"
|
||||
:show-annotations="false"
|
||||
:show-label-title="false"
|
||||
>
|
||||
<template #labels="{toggler}">
|
||||
<KeyValue
|
||||
key="labels"
|
||||
:value="value.instanceLabels"
|
||||
:protected-keys="value.systemLabels || []"
|
||||
:toggle-filter="toggler"
|
||||
:add-label="t('labels.addLabel')"
|
||||
:mode="mode"
|
||||
:read-allowed="false"
|
||||
:value-can-be-empty="true"
|
||||
@input="value.setInstanceLabels($event)"
|
||||
/>
|
||||
</template>
|
||||
</Labels>
|
||||
:read-allowed="false"
|
||||
:value-can-be-empty="true"
|
||||
@update:value="value.setLabels($event)"
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="instanceLabel"
|
||||
:label="t('harvester.tab.instanceLabel')"
|
||||
:weight="-10"
|
||||
>
|
||||
<Banner color="info">
|
||||
<t k="harvester.virtualMachine.instanceLabels.banner" />
|
||||
</Banner>
|
||||
<KeyValue
|
||||
key="instance-labels"
|
||||
:value="value.instanceLabels"
|
||||
:protected-keys="value.systemLabels || []"
|
||||
:toggle-filter="toggler"
|
||||
:add-label="t('labels.addLabel')"
|
||||
:mode="mode"
|
||||
:read-allowed="false"
|
||||
:value-can-be-empty="true"
|
||||
@update:value="value.setInstanceLabels($event)"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
name="advanced"
|
||||
:label="t('harvester.tab.advanced')"
|
||||
@@ -434,7 +443,7 @@ export default {
|
||||
<div class="spacer"></div>
|
||||
<Checkbox
|
||||
v-if="value.cpuPinningFeatureEnabled"
|
||||
v-model="cpuPinning"
|
||||
v-model:value="cpuPinning"
|
||||
class="check"
|
||||
type="checkbox"
|
||||
tooltip-key="harvester.virtualMachine.cpuPinning.tooltip"
|
||||
@@ -465,11 +474,29 @@ export default {
|
||||
:mode="mode"
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
v-if="value.tpmPersistentStateFeatureEnabled && tpmEnabled"
|
||||
v-model:value="tpmPersistentStateEnabled"
|
||||
class="check"
|
||||
type="checkbox"
|
||||
:label="t('harvester.virtualMachine.advancedOptions.tpmPersistentState')"
|
||||
:mode="mode"
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
v-model:value="efiEnabled"
|
||||
class="check"
|
||||
type="checkbox"
|
||||
:label="t('harvester.virtualMachine.efiEnabled')"
|
||||
:label="t('harvester.virtualMachine.advancedOptions.efiEnabled')"
|
||||
:mode="mode"
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
v-if="value.efiPersistentStateFeatureEnabled && efiEnabled"
|
||||
v-model:value="efiPersistentStateEnabled"
|
||||
class="check"
|
||||
type="checkbox"
|
||||
:label="t('harvester.virtualMachine.advancedOptions.efiPersistentState')"
|
||||
:mode="mode"
|
||||
/>
|
||||
|
||||
@@ -478,7 +505,7 @@ export default {
|
||||
v-model:value="secureBoot"
|
||||
class="check"
|
||||
type="checkbox"
|
||||
:label="t('harvester.virtualMachine.secureBoot')"
|
||||
:label="t('harvester.virtualMachine.advancedOptions.secureBoot')"
|
||||
:mode="mode"
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
@@ -21,6 +21,8 @@ import { InterfaceOption, VOLUME_DATA_SOURCE_KIND } from '../config/harvester-ma
|
||||
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
||||
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
||||
import { DATA_ENGINE_V2 } from '../models/harvester/persistentvolumeclaim';
|
||||
import { GIBIBYTE } from '../utils/unit';
|
||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterVolume',
|
||||
@@ -72,7 +74,8 @@ export default {
|
||||
|
||||
data() {
|
||||
if (this.mode === _CREATE) {
|
||||
this.value.spec.volumeMode = 'Block';
|
||||
// default volumeMode to Block
|
||||
this.value.spec.volumeMode = VOLUME_MODE.BLOCK;
|
||||
this.value.spec.accessModes = ['ReadWriteMany'];
|
||||
}
|
||||
|
||||
@@ -86,6 +89,7 @@ export default {
|
||||
imageId,
|
||||
snapshots: [],
|
||||
images: [],
|
||||
GIBIBYTE
|
||||
};
|
||||
},
|
||||
|
||||
@@ -124,6 +128,10 @@ export default {
|
||||
return InterfaceOption;
|
||||
},
|
||||
|
||||
volumeModeOptions() {
|
||||
return Object.values(VOLUME_MODE);
|
||||
},
|
||||
|
||||
imageOption() {
|
||||
return sortBy(
|
||||
this.images
|
||||
@@ -177,6 +185,25 @@ export default {
|
||||
return this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
||||
},
|
||||
|
||||
isLonghornStorageClass() {
|
||||
const selectedSC = this.storageClasses.find((sc) => sc.name === this.value?.spec?.storageClassName) || {};
|
||||
|
||||
return selectedSC && selectedSC.isLonghorn;
|
||||
},
|
||||
|
||||
showVolumeMode() {
|
||||
// we won't let user choose volumeMode if source = vmimage
|
||||
if (!this.value.thirdPartyStorageFeatureEnabled || this.isVMImage || !!this.value?.spec?.storageClassName === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isLonghornStorageClass) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
storageClassOptions() {
|
||||
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
@@ -235,7 +262,43 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'source'(neu) {
|
||||
if (neu === 'url') {
|
||||
this.setBlockVolumeMode();
|
||||
this.deleteVolumeForVmAnnotation();
|
||||
}
|
||||
},
|
||||
'value.spec.storageClassName'() {
|
||||
if (this.isLonghornStorageClass) {
|
||||
this.setBlockVolumeMode();
|
||||
this.deleteVolumeForVmAnnotation();
|
||||
}
|
||||
},
|
||||
'value.spec.volumeMode'(neu) {
|
||||
if (neu === VOLUME_MODE.FILE_SYSTEM) {
|
||||
this.setVolumeForVmAnnotation();
|
||||
} else if (neu === VOLUME_MODE.BLOCK ) {
|
||||
this.deleteVolumeForVmAnnotation();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
setBlockVolumeMode() {
|
||||
this.value.spec.volumeMode = VOLUME_MODE.BLOCK;
|
||||
},
|
||||
|
||||
setVolumeForVmAnnotation() {
|
||||
this.value.setAnnotation(HCI_ANNOTATIONS.VOLUME_FOR_VM, 'true');
|
||||
},
|
||||
|
||||
deleteVolumeForVmAnnotation() {
|
||||
if (this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_FOR_VM]) {
|
||||
delete this.value.metadata.annotations[HCI_ANNOTATIONS.VOLUME_FOR_VM];
|
||||
}
|
||||
},
|
||||
|
||||
getAccessMode() {
|
||||
if (!this.longhornV2LVMSupport) {
|
||||
return ['ReadWriteMany'];
|
||||
@@ -244,10 +307,10 @@ export default {
|
||||
const storageClassName = this.value.spec.storageClassName;
|
||||
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
|
||||
|
||||
let readWriteOnce = this.value.isLvm || this.value.isLonghornV2;
|
||||
let readWriteOnce = this.value.isLvm || (!this.value.thirdPartyStorageFeatureEnabled && this.value.isLonghornV2);
|
||||
|
||||
if (storageClass) {
|
||||
readWriteOnce = storageClass.provisioner === LVM_DRIVER || storageClass.parameters?.dataEngine === DATA_ENGINE_V2;
|
||||
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.value.thirdPartyStorageFeatureEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2);
|
||||
}
|
||||
|
||||
return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany'];
|
||||
@@ -288,7 +351,7 @@ export default {
|
||||
const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize);
|
||||
|
||||
if (imageSize) {
|
||||
this.storage = `${ Math.ceil(imageSize / 1024 / 1024 / 1024) }Gi`;
|
||||
this.storage = `${ Math.ceil(imageSize / 1024 / 1024 / 1024) }${ GIBIBYTE }`;
|
||||
}
|
||||
}
|
||||
this.update();
|
||||
@@ -357,7 +420,7 @@ export default {
|
||||
/>
|
||||
|
||||
<LabeledSelect
|
||||
v-if="source === 'blank'"
|
||||
v-if="isBlank"
|
||||
v-model:value="value.spec.storageClassName"
|
||||
:options="storageClassOptions"
|
||||
:label="t('harvester.storage.storageClass.label')"
|
||||
@@ -367,6 +430,18 @@ export default {
|
||||
@update:value="update"
|
||||
/>
|
||||
|
||||
<LabeledSelect
|
||||
v-if="showVolumeMode"
|
||||
v-model:value="value.spec.volumeMode"
|
||||
:label="t('harvester.volume.volumeMode')"
|
||||
:options="volumeModeOptions"
|
||||
required
|
||||
:disabled="!isCreate"
|
||||
:mode="mode"
|
||||
class="mb-20"
|
||||
@update:value="update"
|
||||
/>
|
||||
|
||||
<UnitInput
|
||||
v-model:value="storage"
|
||||
:label="t('harvester.volume.size')"
|
||||
@@ -377,6 +452,7 @@ export default {
|
||||
:disabled="value?.isLonghornV2 && isEdit"
|
||||
required
|
||||
class="mb-20"
|
||||
:suffix="GIBIBYTE"
|
||||
@update:value="update"
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||
import { GIBIBYTE } from '../../utils/unit';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEditCpuMemory',
|
||||
@@ -30,6 +31,7 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
GIBIBYTE,
|
||||
localCpu: this.cpu,
|
||||
localMemory: this.memory
|
||||
};
|
||||
@@ -63,7 +65,7 @@ export default {
|
||||
if (String(this.localMemory).includes('Gi')) {
|
||||
memory = this.localMemory;
|
||||
} else {
|
||||
memory = `${ this.localMemory }Gi`;
|
||||
memory = `${ this.localMemory }${ GIBIBYTE }`;
|
||||
}
|
||||
if (memory.includes('null')) {
|
||||
memory = null;
|
||||
@@ -113,6 +115,7 @@ export default {
|
||||
:output-modifier="true"
|
||||
:disabled="disabled"
|
||||
required
|
||||
:suffix="GIBIBYTE"
|
||||
class="mb-20"
|
||||
@update:value="change"
|
||||
/>
|
||||
|
||||
@@ -97,7 +97,7 @@ export default {
|
||||
});
|
||||
|
||||
const devices = [
|
||||
...this.otherDevices(this.value.domain.devices.hostDevices || []),
|
||||
...this.otherDevices(this.value?.domain?.devices?.hostDevices || []),
|
||||
...formatted,
|
||||
];
|
||||
|
||||
@@ -121,7 +121,9 @@ export default {
|
||||
return inUse;
|
||||
}
|
||||
|
||||
vm.hostDevices.forEach((device) => {
|
||||
const hostDevices = vm?.hostDevices || [];
|
||||
|
||||
hostDevices.forEach((device) => {
|
||||
inUse[device.name] = { usedBy: [vm.metadata.name] };
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
|
||||
import { MEBIBYTE } from '../../utils/unit';
|
||||
export default {
|
||||
name: 'HarvesterReserved',
|
||||
|
||||
@@ -20,7 +20,7 @@ export default {
|
||||
},
|
||||
|
||||
data() {
|
||||
return { memory: this.reservedMemory };
|
||||
return { MEBIBYTE, memory: this.reservedMemory };
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -47,6 +47,7 @@ export default {
|
||||
:input-exponent="2"
|
||||
:increment="1024"
|
||||
:output-modifier="true"
|
||||
:suffix="MEBIBYTE"
|
||||
@update:value="change"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { randomStr } from '@shell/utils/string';
|
||||
import { clone } from '@shell/utils/object';
|
||||
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import ModalWithCard from '@shell/components/ModalWithCard';
|
||||
|
||||
import { _VIEW } from '@shell/config/query-params';
|
||||
|
||||
import { NAMESPACE } from '@shell/config/types';
|
||||
import { HCI } from '../../types';
|
||||
|
||||
@@ -126,7 +128,6 @@ export default {
|
||||
checkedSsh(val) {
|
||||
// if click on Create a New...
|
||||
if (val.includes(_NEW)) {
|
||||
this.checkedSsh = this.checkedSsh.filter((key) => key !== _NEW);
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
@@ -230,7 +231,9 @@ export default {
|
||||
},
|
||||
|
||||
update() {
|
||||
this.$emit('update:sshKey', clone(this.checkedSsh));
|
||||
const sshKeys = this.checkedSsh.filter((key) => key !== _NEW);
|
||||
|
||||
this.$emit('update:sshKey', sshKeys);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -80,7 +80,7 @@ export default {
|
||||
});
|
||||
|
||||
const devices = [
|
||||
...this.otherDevices(this.value.domain.devices.hostDevices || []),
|
||||
...this.otherDevices(this.value?.domain?.devices?.hostDevices || []),
|
||||
...formatted,
|
||||
];
|
||||
|
||||
@@ -121,7 +121,9 @@ export default {
|
||||
return inUse;
|
||||
}
|
||||
|
||||
vm.hostDevices.forEach((device) => {
|
||||
const hostDevices = vm?.hostDevices || [];
|
||||
|
||||
hostDevices.forEach((device) => {
|
||||
inUse[device.name] = { usedBy: [vm.metadata.name] };
|
||||
});
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@ import UnitInput from '@shell/components/form/UnitInput';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import ModalWithCard from '@shell/components/ModalWithCard';
|
||||
|
||||
import { PVC } from '@shell/config/types';
|
||||
import { PVC, STORAGE_CLASS } from '@shell/config/types';
|
||||
import { clone } from '@shell/utils/object';
|
||||
import { ucFirst, randomStr } from '@shell/utils/string';
|
||||
import { removeObject } from '@shell/utils/array';
|
||||
@@ -17,6 +16,7 @@ import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
|
||||
import { SOURCE_TYPE } from '../../../config/harvester-map';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
||||
import { HCI } from '../../../types';
|
||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||
|
||||
export default {
|
||||
emits: ['update:value'],
|
||||
@@ -62,7 +62,7 @@ export default {
|
||||
|
||||
customVolumeMode: {
|
||||
type: String,
|
||||
default: 'Block'
|
||||
default: VOLUME_MODE.Block
|
||||
},
|
||||
|
||||
customAccessMode: {
|
||||
@@ -116,6 +116,12 @@ export default {
|
||||
return this.mode === _CREATE;
|
||||
},
|
||||
|
||||
defaultStorageClass() {
|
||||
const defaultStorage = this.$store.getters['harvester/all'](STORAGE_CLASS).find((sc) => sc.isDefault);
|
||||
|
||||
return defaultStorage;
|
||||
},
|
||||
|
||||
showVolumeTip() {
|
||||
const imageName = this.getImageDisplayName(this.rows[0]?.image);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { _CREATE } from '@shell/config/query-params';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import { HCI } from '../../../../types';
|
||||
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
||||
import { GIBIBYTE } from '../../../../utils/unit';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEditExisting',
|
||||
@@ -59,6 +60,7 @@ export default {
|
||||
}
|
||||
|
||||
return {
|
||||
GIBIBYTE,
|
||||
VOLUME_TYPE,
|
||||
InterfaceOption,
|
||||
loading: false,
|
||||
@@ -109,6 +111,10 @@ export default {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pvc.isGoldenImageVolume) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pvc.attachVM && isAvailable && pvc.attachVM?.id === this.vm?.id && this.isEdit) {
|
||||
isBeingUsed = false;
|
||||
} else if (pvc.attachVM) {
|
||||
@@ -136,10 +142,12 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// update this.value with existing volume spec, then update to upstream component
|
||||
this.value.accessModes = pvcResource.spec.accessModes[0];
|
||||
this.value.size = pvcResource.spec.resources.requests.storage;
|
||||
this.value.storageClassName = pvcResource.spec.storageClassName;
|
||||
this.value.volumeMode = pvcResource.spec.volumeMode;
|
||||
this.update();
|
||||
},
|
||||
|
||||
'value.type'(neu) {
|
||||
@@ -250,6 +258,7 @@ export default {
|
||||
:label="t('harvester.fields.size')"
|
||||
:mode="mode"
|
||||
:disabled="true"
|
||||
:suffix="GIBIBYTE"
|
||||
@update:value="update"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
|
||||
+59
-4
@@ -12,6 +12,7 @@ import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
||||
import { _VIEW } from '@shell/config/query-params';
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import { ucFirst } from '@shell/utils/string';
|
||||
import { GIBIBYTE } from '../../../../utils/unit';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEditVMImage',
|
||||
@@ -67,6 +68,7 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
GIBIBYTE,
|
||||
VOLUME_TYPE,
|
||||
InterfaceOption,
|
||||
loading: false,
|
||||
@@ -90,7 +92,7 @@ export default {
|
||||
imagesOption() {
|
||||
return this.images.filter((c) => c.isReady).sort((a, b) => a.creationTimestamp > b.creationTimestamp ? -1 : 1).map( (I) => {
|
||||
return {
|
||||
label: `${ I.metadata.namespace }/${ I.spec.displayName }`,
|
||||
label: this.imageOptionLabel(I),
|
||||
value: I.id
|
||||
};
|
||||
});
|
||||
@@ -116,8 +118,46 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
thirdPartyStorageEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
||||
},
|
||||
|
||||
isLonghornV2() {
|
||||
return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.isLonghornV2;
|
||||
},
|
||||
|
||||
selectedImage() {
|
||||
return this.$store.getters['harvester/all'](HCI.IMAGE)?.find( (I) => this.value.image === I.id);
|
||||
},
|
||||
|
||||
imageVirtualSize() {
|
||||
if (this.selectedImage?.virtualSize) {
|
||||
return this.selectedImage.virtualSize.replace(' ', '');
|
||||
}
|
||||
|
||||
return '0';
|
||||
},
|
||||
|
||||
diskSize() {
|
||||
const size = this.value?.size || '0';
|
||||
|
||||
return size;
|
||||
},
|
||||
|
||||
imageVirtualSizeInByte() {
|
||||
return Math.max(this.selectedImage?.status?.size, this.selectedImage?.status?.virtualSize);
|
||||
},
|
||||
|
||||
diskSizeInByte() {
|
||||
return parseSi(this.value?.size || '0');
|
||||
},
|
||||
|
||||
showDiskTooSmallError() {
|
||||
if (!this.thirdPartyStorageEnabled ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.imageVirtualSizeInByte > this.diskSizeInByte;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -149,7 +189,7 @@ export default {
|
||||
minExponent: 3,
|
||||
});
|
||||
|
||||
this.value.size = `${ formatSize }Gi`;
|
||||
this.value.size = `${ formatSize }${ GIBIBYTE }`;
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
@@ -158,7 +198,17 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
imageOptionLabel(image) {
|
||||
let label = `${ image.metadata.namespace }/${ image.spec.displayName }`;
|
||||
|
||||
if (this.thirdPartyStorageEnabled) {
|
||||
label += ` (${ image.imageStorageClass } / ${ image.virtualSize })`;
|
||||
}
|
||||
|
||||
return label;
|
||||
},
|
||||
update() {
|
||||
this.value.hasDiskError = this.showDiskTooSmallError;
|
||||
this.$emit('update');
|
||||
},
|
||||
|
||||
@@ -181,7 +231,7 @@ export default {
|
||||
if (!isIsoImage) {
|
||||
imageSizeGiB = Math.max(imageSizeGiB, 10);
|
||||
}
|
||||
this.value['size'] = `${ imageSizeGiB }Gi`;
|
||||
this.value['size'] = `${ imageSizeGiB }${ GIBIBYTE }`;
|
||||
}
|
||||
|
||||
this.update();
|
||||
@@ -288,7 +338,7 @@ export default {
|
||||
:mode="mode"
|
||||
:required="validateRequired"
|
||||
:disable="isLonghornV2"
|
||||
suffix="GiB"
|
||||
:suffix="GIBIBYTE"
|
||||
@update:value="update"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
@@ -341,5 +391,10 @@ export default {
|
||||
class="mb-20"
|
||||
:label="value.volumeBackups.error.message"
|
||||
/>
|
||||
<Banner
|
||||
v-if="!isView && showDiskTooSmallError"
|
||||
color="error"
|
||||
:label="t('harvester.virtualMachine.volume.vmImageVolumeTip', {diskSize: diskSize, imageVirtualSize: imageVirtualSize})"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -12,6 +12,8 @@ import LabelValue from '@shell/components/LabelValue';
|
||||
import { ucFirst } from '@shell/utils/string';
|
||||
import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storageclass';
|
||||
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
|
||||
import { GIBIBYTE } from '../../../../utils/unit';
|
||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEditVolume',
|
||||
@@ -58,6 +60,7 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
GIBIBYTE,
|
||||
VOLUME_TYPE,
|
||||
InterfaceOption,
|
||||
loading: false,
|
||||
@@ -69,6 +72,10 @@ export default {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('longhornV2LVMSupport');
|
||||
},
|
||||
|
||||
thirdPartyStorageClassEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
||||
},
|
||||
|
||||
encryptionValue() {
|
||||
return ucFirst(String(this.value.isEncrypted));
|
||||
},
|
||||
@@ -89,6 +96,18 @@ export default {
|
||||
return allPVCs.find((P) => P.id === `${ this.namespace }/${ this.value.volumeName }`);
|
||||
},
|
||||
|
||||
showVolumeMode() {
|
||||
if (!this.thirdPartyStorageClassEnabled || !!this.value?.storageClassName === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isLonghornStorageClass) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
isDisabled() {
|
||||
return !this.value.newCreateId && this.isEdit && this.isVirtualType;
|
||||
},
|
||||
@@ -110,7 +129,17 @@ export default {
|
||||
|
||||
isLonghornV2() {
|
||||
return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.isLonghornV2;
|
||||
}
|
||||
},
|
||||
|
||||
isLonghornStorageClass() {
|
||||
const selectedSC = this.storageClasses.find((sc) => sc.name === this.value?.storageClassName) || {};
|
||||
|
||||
return selectedSC && selectedSC.isLonghorn;
|
||||
},
|
||||
|
||||
volumeModeOptions() {
|
||||
return Object.values(VOLUME_MODE);
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -118,6 +147,8 @@ export default {
|
||||
immediate: true,
|
||||
handler(neu) {
|
||||
this.value.accessMode = this.getAccessMode(neu);
|
||||
this.value.volumeMode = this.getVolumeMode(neu, this.value.volumeMode);
|
||||
this.update();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -140,7 +171,7 @@ export default {
|
||||
minExponent: 3,
|
||||
});
|
||||
|
||||
this.value.size = `${ formatSize }Gi`;
|
||||
this.value.size = `${ formatSize }${ GIBIBYTE }`;
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
@@ -149,6 +180,20 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
getVolumeMode(storageClassName, originalVolumeMode) {
|
||||
if (!this.thirdPartyStorageClassEnabled) {
|
||||
return VOLUME_MODE.BLOCK;
|
||||
}
|
||||
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
|
||||
|
||||
// longhorn v1, v2 use block volumeMode
|
||||
if (storageClass && storageClass.isLonghorn) {
|
||||
return VOLUME_MODE.BLOCK;
|
||||
}
|
||||
|
||||
return originalVolumeMode;
|
||||
},
|
||||
|
||||
getAccessMode(storageClassName) {
|
||||
if (!this.longhornV2LVMSupport) {
|
||||
return 'ReadWriteMany';
|
||||
@@ -156,10 +201,10 @@ export default {
|
||||
|
||||
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
|
||||
|
||||
let readWriteOnce = this.value.pvc?.isLvm || this.value.pvc?.isLonghornV2;
|
||||
let readWriteOnce = this.value.pvc?.isLvm || (!this.thirdPartyStorageClassEnabled && this.value.pvc?.isLonghornV2);
|
||||
|
||||
if (storageClass) {
|
||||
readWriteOnce = storageClass.provisioner === LVM_DRIVER || storageClass.parameters?.dataEngine === DATA_ENGINE_V2;
|
||||
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.thirdPartyStorageClassEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2);
|
||||
}
|
||||
|
||||
return readWriteOnce ? 'ReadWriteOnce' : 'ReadWriteMany';
|
||||
@@ -258,6 +303,7 @@ export default {
|
||||
:required="validateRequired"
|
||||
:label="t('harvester.fields.size')"
|
||||
:disabled="isLonghornV2"
|
||||
:suffix="GIBIBYTE"
|
||||
@update:value="update"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
@@ -283,6 +329,29 @@ export default {
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
<div
|
||||
v-if="showVolumeMode"
|
||||
data-testid="input-volume-mode"
|
||||
class="col span-6"
|
||||
>
|
||||
<InputOrDisplay
|
||||
:name="t('harvester.volume.volumeMode')"
|
||||
:value="value.volumeMode"
|
||||
:mode="mode"
|
||||
>
|
||||
<LabeledSelect
|
||||
v-model:value="value.volumeMode"
|
||||
:label="t('harvester.volume.volumeMode')"
|
||||
:mode="mode"
|
||||
:options="volumeModeOptions"
|
||||
:disabled="isEdit"
|
||||
required
|
||||
@update:value="update"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-20">
|
||||
<div
|
||||
v-if="value.volumeEncryptionFeatureEnabled && isView"
|
||||
class="col span-6"
|
||||
@@ -292,11 +361,9 @@ export default {
|
||||
:value="encryptionValue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-20">
|
||||
<div
|
||||
v-if="value.volumeBackups && isView"
|
||||
class="col span-3"
|
||||
class="col span-6"
|
||||
>
|
||||
<LabelValue
|
||||
:name="t('harvester.virtualMachine.volume.readyToUse')"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { isEqual } from 'lodash';
|
||||
import { mapGetters } from 'vuex';
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import { clone, set } from '@shell/utils/object';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
@@ -10,16 +11,12 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
import Labels from '@shell/components/form/Labels';
|
||||
|
||||
import NodeScheduling from '@shell/components/form/NodeScheduling';
|
||||
import PodAffinity from '@shell/components/form/PodAffinity';
|
||||
import VGpuDevices from './VirtualMachineVGpuDevices/index';
|
||||
import UsbDevices from './VirtualMachineUSBDevices/index';
|
||||
import KeyValue from '@shell/components/form/KeyValue';
|
||||
|
||||
import { clear } from '@shell/utils/array';
|
||||
import { clone } from '@shell/utils/object';
|
||||
import { saferDump } from '@shell/utils/create-yaml';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
@@ -65,7 +62,6 @@ export default {
|
||||
PodAffinity,
|
||||
AccessCredentials,
|
||||
Reserved,
|
||||
Labels,
|
||||
PciDevices,
|
||||
RestartVMDialog,
|
||||
UnitInput,
|
||||
@@ -195,7 +191,12 @@ export default {
|
||||
|
||||
return true;
|
||||
},
|
||||
isValidationPassed() {
|
||||
// check if any disk hasDiskError is true
|
||||
const hasError = this.diskRows.some((disk) => disk.hasDiskError === true);
|
||||
|
||||
return !hasError;
|
||||
},
|
||||
showCpuPinningBanner() {
|
||||
if (!this.value.cpuPinningFeatureEnabled) {
|
||||
return false;
|
||||
@@ -210,7 +211,10 @@ export default {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
usbPassthroughEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -407,13 +411,26 @@ export default {
|
||||
}
|
||||
const cloneDeepNewVM = clone(this.value);
|
||||
|
||||
// new VM
|
||||
delete cloneDeepNewVM?.metadata;
|
||||
delete cloneDeepNewVM?.__clone;
|
||||
|
||||
// old VM
|
||||
delete this.cloneVM?.metadata;
|
||||
delete this.cloneVM?.__clone;
|
||||
|
||||
// add empty hostDevices to old VM as CRD does not have it.
|
||||
const devicesObj = this.cloneVM?.spec?.template?.spec?.domain?.devices;
|
||||
|
||||
if (devicesObj && devicesObj.hostDevices === undefined) {
|
||||
set(devicesObj, 'hostDevices', []);
|
||||
}
|
||||
|
||||
const oldVM = JSON.parse(JSON.stringify(this.cloneVM));
|
||||
const newVM = JSON.parse(JSON.stringify(cloneDeepNewVM));
|
||||
|
||||
// we won't show restart dialog in yaml page as we don't have a way to detect change in yaml editor.
|
||||
// only check VM is changed in form page.
|
||||
if (isEqual(oldVM, newVM)) {
|
||||
return;
|
||||
}
|
||||
@@ -422,7 +439,11 @@ export default {
|
||||
this.isOpen = true;
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs.restartDialog.resolve = resolve;
|
||||
if (this?.$refs?.restartDialog) {
|
||||
this.$refs.restartDialog.resolve = resolve;
|
||||
} else {
|
||||
return resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -472,6 +493,7 @@ export default {
|
||||
},
|
||||
|
||||
generateYaml() {
|
||||
this.showYaml = this?.$refs?.vmCruResource?.showYaml || false;
|
||||
this.parseVM();
|
||||
const out = saferDump(this.value);
|
||||
|
||||
@@ -485,10 +507,12 @@ export default {
|
||||
<CruResource
|
||||
v-if="spec"
|
||||
id="vm"
|
||||
ref="vmCruResource"
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:cancel-event="true"
|
||||
:mode="mode"
|
||||
:validation-passed="isValidationPassed"
|
||||
:can-yaml="isSingle ? true : false"
|
||||
:errors="errors"
|
||||
:generate-yaml="generateYaml"
|
||||
@@ -671,7 +695,7 @@ export default {
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
v-if="enabledPCI"
|
||||
v-if="enabledPCI && usbPassthroughEnabled"
|
||||
:label="t('harvester.tab.usbDevices')"
|
||||
name="usbDevices"
|
||||
:weight="-7"
|
||||
@@ -697,39 +721,50 @@ export default {
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="labels"
|
||||
:label="t('generic.labels')"
|
||||
:weight="-9"
|
||||
>
|
||||
<Banner color="info">
|
||||
<t k="harvester.virtualMachine.labels.banner" />
|
||||
</Banner>
|
||||
<KeyValue
|
||||
key="labels"
|
||||
:value="value.labels"
|
||||
:add-label="t('labels.addLabel')"
|
||||
:mode="mode"
|
||||
:read-allowed="false"
|
||||
:value-can-be-empty="true"
|
||||
@update:value="value.setLabels($event)"
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="instanceLabel"
|
||||
:label="t('harvester.tab.instanceLabel')"
|
||||
:weight="-8"
|
||||
:weight="-10"
|
||||
>
|
||||
<Labels
|
||||
:default-container-class="'labels-and-annotations-container'"
|
||||
:value="value"
|
||||
<Banner color="info">
|
||||
<t k="harvester.virtualMachine.instanceLabels.banner" />
|
||||
</Banner>
|
||||
<KeyValue
|
||||
key="instance-labels"
|
||||
:value="value.instanceLabels"
|
||||
:protected-keys="value.systemLabels || []"
|
||||
:toggle-filter="toggler"
|
||||
:add-label="t('labels.addLabel')"
|
||||
:mode="mode"
|
||||
:display-side-by-side="false"
|
||||
:show-annotations="false"
|
||||
:show-label-title="false"
|
||||
>
|
||||
<template #labels="{toggler}">
|
||||
<KeyValue
|
||||
key="labels"
|
||||
:value="value.instanceLabels"
|
||||
:protected-keys="value.systemLabels || []"
|
||||
:toggle-filter="toggler"
|
||||
:add-label="t('labels.addLabel')"
|
||||
:mode="mode"
|
||||
:read-allowed="false"
|
||||
:value-can-be-empty="true"
|
||||
@update:value="value.setInstanceLabels($event)"
|
||||
/>
|
||||
</template>
|
||||
</Labels>
|
||||
:read-allowed="false"
|
||||
:value-can-be-empty="true"
|
||||
@update:value="value.setInstanceLabels($event)"
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="advanced"
|
||||
:label="t('harvester.tab.advanced')"
|
||||
:weight="-9"
|
||||
:weight="-11"
|
||||
>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
@@ -873,11 +908,29 @@ export default {
|
||||
:mode="mode"
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
v-if="value.tpmPersistentStateFeatureEnabled && tpmEnabled"
|
||||
v-model:value="tpmPersistentStateEnabled"
|
||||
class="check"
|
||||
type="checkbox"
|
||||
:label="t('harvester.virtualMachine.advancedOptions.tpmPersistentState')"
|
||||
:mode="mode"
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
v-model:value="efiEnabled"
|
||||
class="check"
|
||||
type="checkbox"
|
||||
:label="t('harvester.virtualMachine.efiEnabled')"
|
||||
:label="t('harvester.virtualMachine.advancedOptions.efiEnabled')"
|
||||
:mode="mode"
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
v-if="value.efiPersistentStateFeatureEnabled && efiEnabled"
|
||||
v-model:value="efiPersistentStateEnabled"
|
||||
class="check"
|
||||
type="checkbox"
|
||||
:label="t('harvester.virtualMachine.advancedOptions.efiPersistentState')"
|
||||
:mode="mode"
|
||||
/>
|
||||
|
||||
@@ -886,9 +939,10 @@ export default {
|
||||
v-model:value="secureBoot"
|
||||
class="check"
|
||||
type="checkbox"
|
||||
:label="t('harvester.virtualMachine.secureBoot')"
|
||||
:label="t('harvester.virtualMachine.advancedOptions.secureBoot')"
|
||||
:mode="mode"
|
||||
/>
|
||||
|
||||
<Banner
|
||||
v-if="showCpuPinningBanner"
|
||||
color="warning"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import ConsumptionGauge from '@shell/components/ConsumptionGauge';
|
||||
import { METRIC, NODE } from '@shell/config/types';
|
||||
import { formatSi, exponentNeeded, UNITS, parseSi } from '@shell/utils/units';
|
||||
|
||||
import { UNIT_SUFFIX } from '../utils/unit';
|
||||
export default {
|
||||
name: 'HarvesterMemoryUsed',
|
||||
components: { ConsumptionGauge },
|
||||
@@ -51,7 +51,7 @@ export default {
|
||||
memoryUnits() {
|
||||
const exponent = exponentNeeded(this.memoryTotal, 1024);
|
||||
|
||||
return `${ UNITS[exponent] }iB`;
|
||||
return `${ UNITS[exponent] }${ UNIT_SUFFIX }`;
|
||||
},
|
||||
|
||||
node() {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import ConsumptionGauge from '@shell/components/ConsumptionGauge';
|
||||
import { LONGHORN } from '@shell/config/types';
|
||||
import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
|
||||
import { UNIT_SUFFIX } from '../utils/unit';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterStorageUsed',
|
||||
@@ -75,13 +76,13 @@ export default {
|
||||
allocatedUnits() {
|
||||
const exponent = exponentNeeded(this.storageStats.total, 1024);
|
||||
|
||||
return `${ UNITS[exponent] }iB`;
|
||||
return `${ UNITS[exponent] }${ UNIT_SUFFIX }`;
|
||||
},
|
||||
|
||||
usedUnits() {
|
||||
const exponent = exponentNeeded(this.storageStats.maximum, 1024);
|
||||
|
||||
return `${ UNITS[exponent] }iB`;
|
||||
return `${ UNITS[exponent] }${ UNIT_SUFFIX }`;
|
||||
},
|
||||
|
||||
formatUsed() {
|
||||
|
||||
@@ -38,6 +38,26 @@ asyncButton:
|
||||
success: Restarted
|
||||
waiting: Restarting…
|
||||
|
||||
dialog:
|
||||
confirmExecution:
|
||||
title: Are you sure?
|
||||
andOthers: |-
|
||||
{count, plural,
|
||||
=0 {}
|
||||
=1 { and <b>one other </b>}
|
||||
other { and <b>{count} other </b>}
|
||||
}
|
||||
protip: "Tip: Hold the {alternateLabel} key while clicking action to bypass this confirmation"
|
||||
stop:
|
||||
message: "Are you sure you want to continue stop the {type} {names}?"
|
||||
pause:
|
||||
message: "Are you sure you want to continue pause the {type} {names}?"
|
||||
promptRemove:
|
||||
title: "Delete {type}"
|
||||
warningMessage: "Deleting the selected {type} permanently removes all resources on {thisOrThese} {type}. This action is irreversible. Do you want to continue?"
|
||||
confirmName: "Type <b>{nameToMatch}</b> to delete the {type}:"
|
||||
protip: "Tip: Hold the {alternateLabel} key while clicking Delete to bypass the confirmation step."
|
||||
|
||||
harvester:
|
||||
branding:
|
||||
logos:
|
||||
@@ -46,6 +66,7 @@ harvester:
|
||||
tip: 'Upload an icon to replace the Harvester favicon in the browser tab. Max file size is 20KB'
|
||||
productLabel: 'Harvester'
|
||||
modal:
|
||||
|
||||
backup:
|
||||
success: 'Backup { backUpName } has been initiated.'
|
||||
addBackup: Add Backup
|
||||
@@ -84,6 +105,14 @@ harvester:
|
||||
tip: Please enter a virtual machine name!
|
||||
success: 'Virtual machine { name } cloned successfully.'
|
||||
failed: 'Failed clone virtual machine!'
|
||||
osImage:
|
||||
title: Delete Image
|
||||
message: Are you sure you want to delete the image { name } ?
|
||||
downloadImage:
|
||||
title: Download Image
|
||||
banner: 'This action takes a while depending on the image size ({ size }). Please be patient.'
|
||||
startMessage : 'The download process will auto start once the conversion is complete.'
|
||||
download: Download
|
||||
exportImage:
|
||||
title: Export to Image
|
||||
name: Name
|
||||
@@ -584,15 +613,17 @@ harvester:
|
||||
enableUsb: Enable USB Tablet
|
||||
advancedOptions:
|
||||
tpm: Enable TPM
|
||||
tpmPersistentState: TPM Persistent State
|
||||
cpuManager:
|
||||
prefix: You must enable CPU Manager for at least one node in
|
||||
middle: 'host page'
|
||||
suffix: to enable CPU Pinning for VM
|
||||
efiEnabled: Booting in EFI mode
|
||||
efiPersistentState: EFI Persistent State
|
||||
secureBoot: Secure Boot
|
||||
usbTip: Provides an absolute pointer device which often helps with getting a consistent mouse cursor position in VNC.
|
||||
sshTitle: Add Public SSH Key
|
||||
imageTip: An external URL to the .iso, .img, .qcow2 or .raw that the virtual machine should be created from.
|
||||
efiEnabled: Booting in EFI mode
|
||||
secureBoot: Secure Boot
|
||||
volume:
|
||||
dragTip: Drag and drop volumes, or use the volume's arrows, to change the boot order.
|
||||
volumeTip: The virtual machine only contains a CD-ROM volume. You may want to add additional disk volumes.
|
||||
@@ -613,6 +644,7 @@ harvester:
|
||||
setFirst: Set as root volume
|
||||
saveVolume: Update Volume
|
||||
encryption: Encryption
|
||||
vmImageVolumeTip: Disk size ({diskSize}) should greater than selected image virtual size ({imageVirtualSize})
|
||||
lockTooltip:
|
||||
all: All volumes are encrypted.
|
||||
partial: Some volumes are encrypted.
|
||||
@@ -755,6 +787,10 @@ harvester:
|
||||
inNamespaces: "Workloads in these namespaces"
|
||||
namespaces:
|
||||
label: Namespaces
|
||||
instanceLabels:
|
||||
banner: These labels are automatically synchronized to the virtual machine instance.
|
||||
labels:
|
||||
banner: These key values are added as labels to the virtual machine.
|
||||
|
||||
volume:
|
||||
label: Volumes
|
||||
@@ -764,6 +800,7 @@ harvester:
|
||||
datasource: Data Source
|
||||
details: Details
|
||||
size: Size
|
||||
volumeMode: Volume Mode
|
||||
source: Source
|
||||
kind: Kind
|
||||
sourceOptions:
|
||||
@@ -814,6 +851,8 @@ harvester:
|
||||
=1 {1 image is uploading, please do not refresh or close the page.}
|
||||
other {{count} images are uploading, please do not refresh or close the page.}
|
||||
}
|
||||
osUpgrade:
|
||||
uploading: "{name} is uploading, please do not refresh or close the page."
|
||||
checksum: Checksum
|
||||
checksumTip: Validate the image using the SHA512 checksum, if specified.
|
||||
|
||||
@@ -838,17 +877,22 @@ harvester:
|
||||
currentVersion: CURRENT VERSION
|
||||
versionLabel: AVAILABLE COMPATIBLE VERSIONS
|
||||
enableLogging: Enable Logging
|
||||
skipSingleReplicaDetachedVol: Skip checking single-replica detached volumes
|
||||
upgradeNode: Upgrading Node
|
||||
upgradeSysService: Upgrading System Service
|
||||
upgradeImage: Download Upgrade Image
|
||||
osUpgrade: OS Upgrade
|
||||
osUpgrade: Cluster Upgrade
|
||||
uploadNew: Upload New Image
|
||||
deleteHeader: Please select an image to delete.
|
||||
deleteExisting: Delete Existing Image
|
||||
selectExisting: Select Existing Image
|
||||
createRepository: Creating Upgrade Repository
|
||||
succeeded: Succeeded
|
||||
releaseTip: Please read the upgrade documentation carefully. You can view details on the <a href="{url}" target="_blank">Harvester Release Notes</a>.
|
||||
checkReady: I have read and understood the upgrade instructions related to this Harvester version.
|
||||
pending: Pending
|
||||
upload:
|
||||
duplicatedFile: The file you are trying to upload already exists.
|
||||
repoInfo:
|
||||
upgradeStatus: Upgrade Status
|
||||
os: OS
|
||||
@@ -866,14 +910,14 @@ harvester:
|
||||
doc: Read the <a href="{url}" target="_blank">documentation</a> before starting the upgrade process. Ensure that you complete procedures that are relevant to your environment and the version you are upgrading to.
|
||||
tip: Unmet system requirements and incorrectly performed procedures may cause complete upgrade failure and other issues that require manual workarounds.
|
||||
moreNotes: For more details about the release notes, please visit -
|
||||
|
||||
|
||||
schedule:
|
||||
label: Virtual Machine Schedules
|
||||
createTitle: Create Schedule
|
||||
createButtonText: Create Schedule
|
||||
scheduleType: Virtual Machine Schedule Type
|
||||
cron: Cron Schedule
|
||||
detail:
|
||||
detail:
|
||||
namespace: Namespace
|
||||
sourceVM: Source Virtual Machine
|
||||
tabs:
|
||||
@@ -883,12 +927,12 @@ harvester:
|
||||
message:
|
||||
noSetting:
|
||||
suffix: before creating a backup schedule
|
||||
retain:
|
||||
retain:
|
||||
label: Retain
|
||||
count: Count
|
||||
tooltip: Number of up-to-date VM backups to retain. Maximum to 250, minimum to 2.
|
||||
tooltip: Number of up-to-date VM backups to retain. Maximum to 250, minimum to 2.
|
||||
maxFailure:
|
||||
label: Max Failure
|
||||
label: Max Failure
|
||||
count: Count
|
||||
tooltip: Max number of consecutive failed backups that could be tolerated. If reach this threshold, Harvester controller will suspend the schedule job. This value should less than retain count
|
||||
virtualMachine:
|
||||
@@ -1014,7 +1058,7 @@ harvester:
|
||||
invalid: '"Exclude list" is invalid.'
|
||||
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.'
|
||||
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 4 + 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>.'
|
||||
vmForceDeletionPolicy:
|
||||
period: Period
|
||||
ratio : Ratio
|
||||
@@ -1051,10 +1095,15 @@ harvester:
|
||||
addConfig: Add Configuration
|
||||
|
||||
upgrade:
|
||||
selectExitImage: Please select the OS image to upgrade.
|
||||
unknownImageName: Image name is not found.
|
||||
imageUrl: Please input a valid image URL.
|
||||
chooseFile: Please select to upload an image.
|
||||
checksum: Checksum
|
||||
networkError: Unable to upload the image. Resolve network issues that may have occurred and try again.
|
||||
cancelUpload: Cancelled the image upload.
|
||||
uploadSuccess: "{name} uploaded successfully. Press Upgrade button to start the cluster upgrade process."
|
||||
deleteImage: Please select an image to delete.
|
||||
deleteSuccess: "{name} deleted successfully."
|
||||
harvesterMonitoring:
|
||||
label: Harvester Monitoring
|
||||
section:
|
||||
|
||||
@@ -8,9 +8,7 @@ import Banner from '@components/Banner/Banner.vue';
|
||||
import MessageLink from '@shell/components/MessageLink';
|
||||
import SortableTable from '@shell/components/SortableTable';
|
||||
import { allHash, setPromiseResult } from '@shell/utils/promise';
|
||||
import {
|
||||
parseSi, formatSi, exponentNeeded, UNITS, createMemoryValues
|
||||
} from '@shell/utils/units';
|
||||
import { parseSi, formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
|
||||
import { REASON } from '@shell/config/table-headers';
|
||||
import {
|
||||
EVENT, METRIC, NODE, SERVICE, PVC, LONGHORN, POD, COUNT, NETWORK_ATTACHMENT
|
||||
@@ -27,22 +25,21 @@ import { isEmpty } from '@shell/utils/object';
|
||||
import { HCI } from '../types';
|
||||
import HarvesterUpgrade from '../components/HarvesterUpgrade';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
||||
import { UNIT_SUFFIX } from '../utils/unit';
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(minMax);
|
||||
|
||||
const PARSE_RULES = {
|
||||
memory: {
|
||||
format: {
|
||||
addSuffix: true,
|
||||
firstSuffix: 'B',
|
||||
increment: 1024,
|
||||
maxExponent: 99,
|
||||
maxPrecision: 2,
|
||||
minExponent: 0,
|
||||
startingExponent: 0,
|
||||
suffix: 'iB',
|
||||
}
|
||||
format: {
|
||||
addSuffix: true,
|
||||
firstSuffix: UNIT_SUFFIX,
|
||||
increment: 1024,
|
||||
maxExponent: 99,
|
||||
maxPrecision: 2,
|
||||
minExponent: 0,
|
||||
startingExponent: 0,
|
||||
suffix: UNIT_SUFFIX,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -135,6 +132,7 @@ export default {
|
||||
|
||||
const hash = {
|
||||
vms: this.fetchClusterResources(HCI.VM),
|
||||
pvcs: this.fetchClusterResources(PVC),
|
||||
nodes: this.fetchClusterResources(NODE),
|
||||
events: this.fetchClusterResources(EVENT),
|
||||
metricNodes: this.fetchClusterResources(METRIC.NODE),
|
||||
@@ -224,6 +222,7 @@ export default {
|
||||
nodes: [],
|
||||
metricNodes: [],
|
||||
vms: [],
|
||||
pvcs: [],
|
||||
monitoring: {},
|
||||
VM_DASHBOARD_METRICS_URL,
|
||||
CLUSTER_METRICS_SUMMARY_URL,
|
||||
@@ -288,6 +287,14 @@ export default {
|
||||
out[resource.type].name = this.t(`typeLabel."${ resource.spoofed.name }"`, { count: out[resource.type].total });
|
||||
}
|
||||
|
||||
if (resource.type === PVC) {
|
||||
// filter out the golden image volumes
|
||||
const goldenImageVolumeCount = (this.pvcs || []).filter((pvc) => pvc.isGoldenImageVolume).length;
|
||||
|
||||
out[resource.type].useful = out[resource.type].useful - goldenImageVolumeCount;
|
||||
out[resource.type].total = out[resource.type].total - goldenImageVolumeCount;
|
||||
}
|
||||
|
||||
if (resource.type === HCI.BLOCK_DEVICE) {
|
||||
let total = 0;
|
||||
let errorCount = 0;
|
||||
@@ -402,13 +409,13 @@ export default {
|
||||
storageUsed() {
|
||||
const stats = this.storageStats;
|
||||
|
||||
return this.createMemoryValues(stats.maximum, stats.used);
|
||||
return this.createDisplayValues(stats.maximum, stats.used);
|
||||
},
|
||||
|
||||
storageAllocated() {
|
||||
const stats = this.storageStats;
|
||||
|
||||
return this.createMemoryValues(stats.total, stats.scheduled);
|
||||
return this.createDisplayValues(stats.total, stats.scheduled);
|
||||
},
|
||||
|
||||
vmEvents() {
|
||||
@@ -454,7 +461,7 @@ export default {
|
||||
return total + node.memoryReserved;
|
||||
}, 0);
|
||||
|
||||
return createMemoryValues(this.memoryTotal, useful);
|
||||
return this.createDisplayValues(this.memoryTotal, useful);
|
||||
},
|
||||
|
||||
availableNodes() {
|
||||
@@ -494,7 +501,7 @@ export default {
|
||||
},
|
||||
|
||||
ramUsed() {
|
||||
return createMemoryValues(this.memoryTotal, this.metricAggregations?.memory);
|
||||
return this.createDisplayValues(this.memoryTotal, this.metricAggregations?.memory);
|
||||
},
|
||||
|
||||
hasMetricNodeSchema() {
|
||||
@@ -516,10 +523,12 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
createMemoryValues(total, useful) {
|
||||
createDisplayValues(total, useful) {
|
||||
const parsedTotal = parseSi((total || '0').toString());
|
||||
|
||||
const parsedUseful = parseSi((useful || '0').toString());
|
||||
const format = this.createMemoryFormat(parsedTotal);
|
||||
const format = this.createFormat(parsedTotal);
|
||||
|
||||
const formattedTotal = formatSi(parsedTotal, format);
|
||||
let formattedUseful = formatSi(parsedUseful, {
|
||||
...format,
|
||||
@@ -538,24 +547,24 @@ export default {
|
||||
useful: Number(parsedUseful),
|
||||
formattedTotal,
|
||||
formattedUseful,
|
||||
units: this.createMemoryUnits(parsedTotal),
|
||||
units: this.createUnits(parsedTotal),
|
||||
};
|
||||
},
|
||||
|
||||
createMemoryFormat(n) {
|
||||
const exponent = exponentNeeded(n, PARSE_RULES.memory.format.increment);
|
||||
createFormat(n) {
|
||||
const exponent = exponentNeeded(n, PARSE_RULES.format.increment);
|
||||
|
||||
return {
|
||||
...PARSE_RULES.memory.format,
|
||||
...PARSE_RULES.format,
|
||||
maxExponent: exponent,
|
||||
minExponent: exponent,
|
||||
};
|
||||
},
|
||||
|
||||
createMemoryUnits(n) {
|
||||
const exponent = exponentNeeded(n, PARSE_RULES.memory.format.increment);
|
||||
createUnits(n) {
|
||||
const exponent = exponentNeeded(n, PARSE_RULES.format.increment);
|
||||
|
||||
return `${ UNITS[exponent] }${ PARSE_RULES.memory.format.suffix }`;
|
||||
return `${ UNITS[exponent] }${ PARSE_RULES.format.suffix }`;
|
||||
},
|
||||
|
||||
async fetchClusterResources(type, opt = {}, store) {
|
||||
|
||||
@@ -101,14 +101,14 @@ export default {
|
||||
const metricCol = [
|
||||
{
|
||||
name: 'cpu',
|
||||
labelKey: 'node.detail.glance.consumptionGauge.cpu',
|
||||
labelKey: 'harvester.dashboard.hardwareResourceGauge.cpu',
|
||||
value: 'id',
|
||||
formatter: 'HarvesterCPUUsed',
|
||||
formatterOpts: { showUsed: true },
|
||||
},
|
||||
{
|
||||
name: 'memory',
|
||||
labelKey: 'node.detail.glance.consumptionGauge.memory',
|
||||
labelKey: 'harvester.dashboard.hardwareResourceGauge.memory',
|
||||
value: 'id',
|
||||
formatter: 'HarvesterMemoryUsed',
|
||||
formatterOpts: { showUsed: true },
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { Banner } from '@components/Banner';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import { VIEW_IN_API, DEV } from '@shell/store/prefs';
|
||||
import { MANAGEMENT } from '@shell/config/types';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import Tabbed from '@shell/components/Tabbed/index.vue';
|
||||
import Tab from '@shell/components/Tabbed/Tab.vue';
|
||||
import Settings from '@pkg/harvester/components/SettingList.vue';
|
||||
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING } from '../config/settings';
|
||||
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
|
||||
import { HCI } from '../types';
|
||||
|
||||
export default {
|
||||
@@ -21,14 +20,6 @@ export default {
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
let isDev;
|
||||
|
||||
try {
|
||||
isDev = this.$store.getters['prefs/get'](VIEW_IN_API);
|
||||
} catch {
|
||||
isDev = this.$store.getters['prefs/get'](DEV);
|
||||
}
|
||||
|
||||
const isSingleProduct = !!this.$store.getters['isSingleProduct'];
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
@@ -77,7 +68,7 @@ export default {
|
||||
};
|
||||
|
||||
s.hide = s.canHide = (s.kind === 'json' || s.kind === 'multiline' || s.customFormatter === 'json' || s.data.customFormatter === 'json');
|
||||
s.hasActions = !s.readOnly || isDev;
|
||||
s.hasActions = s.id === HCI_SETTING.SERVER_VERSION ? true : !s.readOnly;
|
||||
initSettings.push(s);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
} from '@shell/config/types';
|
||||
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
|
||||
import HarvesterVolumeState from '../formatters/HarvesterVolumeState';
|
||||
|
||||
import { allSettled } from '../utils/promise';
|
||||
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
||||
|
||||
@@ -58,7 +57,6 @@ export default {
|
||||
if (!pvcSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) {
|
||||
this.$store.dispatch('type-map/configureType', { match: HCI.VOLUME, isCreatable: false });
|
||||
}
|
||||
|
||||
this.rows = hash.pvcs;
|
||||
},
|
||||
|
||||
@@ -70,7 +68,10 @@ export default {
|
||||
schema() {
|
||||
return schema;
|
||||
},
|
||||
|
||||
filterRows() {
|
||||
// we only show the non golden image PVCs in the list
|
||||
return this.rows.filter((pvc) => !pvc?.isGoldenImageVolume);
|
||||
},
|
||||
headers() {
|
||||
return [
|
||||
STATE,
|
||||
@@ -92,7 +93,8 @@ export default {
|
||||
{
|
||||
name: 'storageClass',
|
||||
labelKey: 'tableHeaders.storageClass',
|
||||
value: 'spec.storageClassName'
|
||||
value: 'spec.storageClassName',
|
||||
sort: 'spec.storageClassName'
|
||||
},
|
||||
{
|
||||
name: 'AttachedVM',
|
||||
@@ -146,7 +148,7 @@ export default {
|
||||
:groupable="true"
|
||||
default-sort-by="age"
|
||||
:namespaced="true"
|
||||
:rows="rows"
|
||||
:rows="filterRows"
|
||||
:schema="schema"
|
||||
key-field="_key"
|
||||
>
|
||||
|
||||
@@ -137,7 +137,15 @@ export default {
|
||||
},
|
||||
|
||||
isTpmEnabled(spec) {
|
||||
return !!spec?.template?.spec?.domain?.devices?.tpm ;
|
||||
return !!spec?.template?.spec?.domain?.devices?.tpm;
|
||||
},
|
||||
|
||||
isTPMPersistentStateEnabled(spec) {
|
||||
return !!spec?.template?.spec?.domain?.devices?.tpm?.persistent;
|
||||
},
|
||||
|
||||
isEFIPersistentStateEnabled(spec) {
|
||||
return !!spec?.template?.spec?.domain?.firmware?.bootloader?.efi?.persistent;
|
||||
},
|
||||
|
||||
isSecureBoot(spec) {
|
||||
|
||||
@@ -25,6 +25,8 @@ import { HCI_SETTING } from '../../config/settings';
|
||||
import { HCI } from '../../types';
|
||||
import { parseVolumeClaimTemplates } from '../../utils/vm';
|
||||
import impl, { QGA_JSON, USB_TABLET } from './impl';
|
||||
import { GIBIBYTE } from '../../utils/unit';
|
||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||
|
||||
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
|
||||
|
||||
@@ -138,6 +140,7 @@ export default {
|
||||
return {
|
||||
OS,
|
||||
isClone,
|
||||
showYaml: false,
|
||||
spec: null,
|
||||
osType: 'linux',
|
||||
sshKey: [],
|
||||
@@ -164,6 +167,8 @@ export default {
|
||||
accessCredentials: [],
|
||||
efiEnabled: false,
|
||||
tpmEnabled: false,
|
||||
tpmPersistentStateEnabled: false,
|
||||
efiPersistentStateEnabled: false,
|
||||
secureBoot: false,
|
||||
userDataTemplateId: '',
|
||||
saveUserDataAsClearText: false,
|
||||
@@ -217,7 +222,7 @@ export default {
|
||||
const selectedVMNetworks = networkNames.map((name) => vmNetworks.find((n) => n.id === name)).filter((n) => n?.id);
|
||||
const clusterNetworks = uniq(selectedVMNetworks.map((n) => n.clusterNetworkResource?.id));
|
||||
|
||||
return nodes.filter((N) => !N.isUnSchedulable).map((node) => {
|
||||
return nodes.filter((N) => !N.isUnSchedulable && N.isEtcd !== 'true').map((node) => {
|
||||
const requireLabelKeys = [];
|
||||
let isNetworkSchedule = true;
|
||||
|
||||
@@ -241,12 +246,6 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
defaultStorageClass() {
|
||||
const defaultStorage = this.$store.getters[`${ this.inStore }/all`](STORAGE_CLASS).find( (O) => O.isDefault);
|
||||
|
||||
return defaultStorage;
|
||||
},
|
||||
|
||||
storageClassSetting() {
|
||||
try {
|
||||
const storageClassValue = this.$store.getters[`${ this.inStore }/all`](HCI.SETTING).find( (O) => O.id === HCI_SETTING.DEFAULT_STORAGE_CLASS)?.value;
|
||||
@@ -258,7 +257,7 @@ export default {
|
||||
},
|
||||
|
||||
customVolumeMode() {
|
||||
return this.storageClassSetting.volumeMode || 'Block';
|
||||
return this.storageClassSetting.volumeMode || VOLUME_MODE.BLOCK;
|
||||
},
|
||||
|
||||
customAccessMode() {
|
||||
@@ -271,7 +270,7 @@ export default {
|
||||
|
||||
needNewSecret() {
|
||||
// When creating a template it is always necessary to create a new secret.
|
||||
return this.resourceType === HCI.VM_VERSION || this.isCreate;
|
||||
return this.showYaml ? false : this.resourceType === HCI.VM_VERSION || this.isCreate;
|
||||
},
|
||||
|
||||
defaultTerminationSetting() {
|
||||
@@ -367,6 +366,8 @@ export default {
|
||||
const installAgent = this.hasInstallAgent(userData, osType, true);
|
||||
const efiEnabled = this.isEfiEnabled(spec);
|
||||
const tpmEnabled = this.isTpmEnabled(spec);
|
||||
const tpmPersistentStateEnabled = this.isTPMPersistentStateEnabled(spec);
|
||||
const efiPersistentStateEnabled = this.isEFIPersistentStateEnabled(spec);
|
||||
const secureBoot = this.isSecureBoot(spec);
|
||||
const cpuPinning = this.isCpuPinning(spec);
|
||||
|
||||
@@ -398,7 +399,9 @@ export default {
|
||||
|
||||
this['installUSBTablet'] = installUSBTablet;
|
||||
this['efiEnabled'] = efiEnabled;
|
||||
this['efiPersistentStateEnabled'] = efiPersistentStateEnabled;
|
||||
this['tpmEnabled'] = tpmEnabled;
|
||||
this['tpmPersistentStateEnabled'] = tpmPersistentStateEnabled;
|
||||
this['secureBoot'] = secureBoot;
|
||||
this['cpuPinning'] = cpuPinning;
|
||||
|
||||
@@ -442,7 +445,7 @@ export default {
|
||||
if (!isIsoImage) {
|
||||
imageSizeGiB = Math.max(imageSizeGiB, 10);
|
||||
}
|
||||
size = `${ imageSizeGiB }Gi`;
|
||||
size = `${ imageSizeGiB }${ GIBIBYTE }`;
|
||||
}
|
||||
|
||||
out.push({
|
||||
@@ -456,7 +459,7 @@ export default {
|
||||
type,
|
||||
storageClassName: '',
|
||||
image: this.imageId,
|
||||
volumeMode: 'Block',
|
||||
volumeMode: VOLUME_MODE.BLOCK,
|
||||
isEncrypted,
|
||||
volumeBackups,
|
||||
});
|
||||
@@ -515,7 +518,7 @@ export default {
|
||||
accessMode = pvcResource?.spec?.accessModes?.[0] || 'ReadWriteMany';
|
||||
size = pvcResource?.spec?.resources?.requests?.storage || '10Gi';
|
||||
storageClassName = pvcResource?.spec?.storageClassName;
|
||||
volumeMode = pvcResource?.spec?.volumeMode || 'Block';
|
||||
volumeMode = pvcResource?.spec?.volumeMode || VOLUME_MODE.BLOCK;
|
||||
volumeName = pvcResource?.metadata?.name || '';
|
||||
}
|
||||
|
||||
@@ -552,7 +555,7 @@ export default {
|
||||
volumeName,
|
||||
container,
|
||||
accessMode,
|
||||
size: `${ formatSize }Gi`,
|
||||
size: `${ formatSize }${ GIBIBYTE }`,
|
||||
volumeMode: volumeMode || this.customVolumeMode,
|
||||
image,
|
||||
type,
|
||||
@@ -667,7 +670,7 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.secretName || this.needNewSecret) {
|
||||
if (this.needNewSecret || !this.secretName) {
|
||||
this.secretName = this.generateSecretName(this.secretNamePrefix);
|
||||
}
|
||||
|
||||
@@ -869,7 +872,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
if (out.length === 0 && !!this.spec.template.spec.accessCredentials) {
|
||||
if (out.length === 0 && !!this.spec.template.spec.accessCredentials === false) {
|
||||
delete this.spec.template.spec.accessCredentials;
|
||||
} else {
|
||||
this.spec.template.spec.accessCredentials = out;
|
||||
@@ -969,7 +972,7 @@ export default {
|
||||
|
||||
parseVolumeClaimTemplate(R, dataVolumeName) {
|
||||
if (!String(R.size).includes('Gi') && R.size) {
|
||||
R.size = `${ R.size }Gi`;
|
||||
R.size = `${ R.size }${ GIBIBYTE }`;
|
||||
}
|
||||
|
||||
const out = {
|
||||
@@ -1384,13 +1387,21 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
setBootMethod(boot = { efi: false, secureBoot: false }) {
|
||||
if (boot.efi && boot.secureBoot) {
|
||||
set(this.spec.template.spec.domain, 'features.smm.enabled', true);
|
||||
set(this.spec.template.spec.domain, 'firmware.bootloader.efi.secureBoot', true);
|
||||
} else if (boot.efi && !boot.secureBoot) {
|
||||
// set(this.spec.template.spec.domain, 'features.smm.enabled', false);
|
||||
setBootMethod(boot = {
|
||||
efi: false, secureBoot: false, efiPersistentStateEnabled: false
|
||||
}) {
|
||||
if (boot.efi) {
|
||||
set(this.spec.template.spec.domain, 'firmware.bootloader.efi.secureBoot', boot.secureBoot);
|
||||
} else {
|
||||
delete this.spec.template.spec.domain['firmware'];
|
||||
delete this.spec.template.spec.domain.features['smm'];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (boot.secureBoot) {
|
||||
set(this.spec.template.spec.domain, 'features.smm.enabled', true);
|
||||
} else {
|
||||
try {
|
||||
delete this.spec.template.spec.domain.features.smm['enabled'];
|
||||
const noKeys = Object.keys(this.spec.template.spec.domain.features.smm).length === 0;
|
||||
@@ -1399,10 +1410,12 @@ export default {
|
||||
delete this.spec.template.spec.domain.features['smm'];
|
||||
}
|
||||
} catch (e) {}
|
||||
set(this.spec.template.spec.domain, 'firmware.bootloader.efi.secureBoot', false);
|
||||
}
|
||||
|
||||
if (boot.efiPersistentStateEnabled) {
|
||||
set(this.spec.template.spec.domain, 'firmware.bootloader.efi.persistent', true);
|
||||
} else {
|
||||
delete this.spec.template.spec.domain['firmware'];
|
||||
delete this.spec.template.spec.domain.features['smm'];
|
||||
delete this.spec.template.spec.domain.firmware.bootloader.efi['persistent'];
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1414,9 +1427,9 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
setTPM(tpmEnabled) {
|
||||
setTPM({ tpmEnabled = false, tpmPersistentStateEnabled = false } = {}) {
|
||||
if (tpmEnabled) {
|
||||
set(this.spec.template.spec.domain.devices, 'tpm', {});
|
||||
set(this.spec.template.spec.domain.devices, 'tpm', tpmPersistentStateEnabled ? { persistent: true } : {});
|
||||
} else {
|
||||
delete this.spec.template.spec.domain.devices['tpm'];
|
||||
}
|
||||
@@ -1529,11 +1542,21 @@ export default {
|
||||
},
|
||||
|
||||
efiEnabled(val) {
|
||||
this.setBootMethod({ efi: val, secureBoot: this.secureBoot });
|
||||
this.setBootMethod({
|
||||
efi: val, secureBoot: this.secureBoot, efiPersistentStateEnabled: this.efiPersistentStateEnabled
|
||||
});
|
||||
},
|
||||
|
||||
secureBoot(val) {
|
||||
this.setBootMethod({ efi: this.efiEnabled, secureBoot: val });
|
||||
this.setBootMethod({
|
||||
efi: this.efiEnabled, secureBoot: val, efiPersistentStateEnabled: this.efiPersistentStateEnabled
|
||||
});
|
||||
},
|
||||
|
||||
efiPersistentStateEnabled(val) {
|
||||
this.setBootMethod({
|
||||
efi: this.efiEnabled, secureBoot: this.secureBoot, efiPersistentStateEnabled: val
|
||||
});
|
||||
},
|
||||
|
||||
cpuPinning(value) {
|
||||
@@ -1541,7 +1564,11 @@ export default {
|
||||
},
|
||||
|
||||
tpmEnabled(val) {
|
||||
this.setTPM(val);
|
||||
this.setTPM({ tpmEnabled: val, tpmPersistentStateEnabled: this.tpmPersistentStateEnabled });
|
||||
},
|
||||
|
||||
tpmPersistentStateEnabled(val) {
|
||||
this.setTPM({ tpmEnabled: this.tpmEnabled, tpmPersistentStateEnabled: val });
|
||||
},
|
||||
|
||||
installAgent: {
|
||||
|
||||
@@ -67,8 +67,7 @@ export default class HciNamespace extends namespace {
|
||||
promptRemove(resources = this) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources,
|
||||
warningMessageKey: 'promptRemove.confirmRelatedResource',
|
||||
component: 'ConfirmRelatedToRemoveDialog'
|
||||
component: 'ConfirmRelatedToRemoveDialog'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -115,8 +115,7 @@ export default class HciNode extends HarvesterResource {
|
||||
promptRemove(resources = this) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources,
|
||||
warningMessageKey: 'promptRemove.confirmRelatedResource',
|
||||
component: 'ConfirmRelatedToRemoveDialog'
|
||||
component: 'ConfirmRelatedToRemoveDialog'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -186,9 +185,16 @@ export default class HciNode extends HarvesterResource {
|
||||
}
|
||||
|
||||
get isKVMDisable() {
|
||||
// Arm based
|
||||
const isARMMachine = this.metadata.labels?.[HCI_ANNOTATIONS.K8S_ARCH]?.includes('arm');
|
||||
|
||||
if (isARMMachine) {
|
||||
return this.status.capacity['devices.kubevirt.io/kvm'] && this.status.capacity['devices.kubevirt.io/kvm'] === '0';
|
||||
}
|
||||
|
||||
const allNotExist = !this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_AMD_CPU] && !this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_INTEL_CPU];
|
||||
|
||||
return this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_AMD_CPU] === 'false' || this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_INTEL_CPU] === 'false' || allNotExist;
|
||||
return allNotExist || this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_AMD_CPU] === 'false' || this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_INTEL_CPU] === 'false';
|
||||
}
|
||||
|
||||
get stateDisplay() {
|
||||
@@ -470,7 +476,7 @@ export default class HciNode extends HarvesterResource {
|
||||
get canDelete() {
|
||||
const nodes = this.$rootGetters['harvester/all'](NODE) || [];
|
||||
|
||||
return nodes.length > 1;
|
||||
return nodes.length > 1 && super.canDelete;
|
||||
}
|
||||
|
||||
get vlanStatuses() {
|
||||
|
||||
@@ -46,22 +46,37 @@ export default class HciPv extends HarvesterResource {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.isLonghorn || !this.isLonghornV2) {
|
||||
const exportImageAction = {
|
||||
action: 'exportImage',
|
||||
enabled: this.hasAction('export') && !this.isEncrypted,
|
||||
icon: 'icon icon-copy',
|
||||
label: this.t('harvester.action.exportImage')
|
||||
};
|
||||
const takeSnapshotAction = {
|
||||
action: 'snapshot',
|
||||
enabled: this.hasAction('snapshot'),
|
||||
icon: 'icon icon-backup',
|
||||
label: this.t('harvester.action.snapshot'),
|
||||
};
|
||||
|
||||
if (this.thirdPartyStorageFeatureEnabled) { // v1.5.0
|
||||
out = [
|
||||
{
|
||||
action: 'exportImage',
|
||||
enabled: this.hasAction('export') && !this.isEncrypted,
|
||||
icon: 'icon icon-copy',
|
||||
label: this.t('harvester.action.exportImage')
|
||||
},
|
||||
{
|
||||
action: 'snapshot',
|
||||
enabled: this.hasAction('snapshot'),
|
||||
icon: 'icon icon-backup',
|
||||
label: this.t('harvester.action.snapshot'),
|
||||
},
|
||||
exportImageAction,
|
||||
takeSnapshotAction,
|
||||
...out
|
||||
];
|
||||
// TODO: remove this block if Longhorn V2 engine supports restore volume snapshot
|
||||
if (this.isLonghornV2) {
|
||||
out = out.filter((action) => action.action !== takeSnapshotAction.action);
|
||||
}
|
||||
} else { // v1.4 / v1.3
|
||||
if (!this.isLonghorn || !this.isLonghornV2) {
|
||||
out = [
|
||||
exportImageAction,
|
||||
takeSnapshotAction,
|
||||
...out
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -321,15 +336,31 @@ export default class HciPv extends HarvesterResource {
|
||||
return this.volumeProvider === LONGHORN_DRIVER;
|
||||
}
|
||||
|
||||
get isLonghornV1() {
|
||||
return this.isLonghorn && !this.isLonghornV2;
|
||||
}
|
||||
|
||||
get isLonghornV2() {
|
||||
return this.dataEngine === DATA_ENGINE_V2;
|
||||
}
|
||||
|
||||
get isGoldenImageVolume() {
|
||||
return this?.metadata?.annotations?.[HCI_ANNOTATIONS.GOLDEN_IMAGE] === 'true';
|
||||
}
|
||||
|
||||
get thirdPartyStorageFeatureEnabled() {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
||||
}
|
||||
|
||||
get resourceExternalLink() {
|
||||
const host = window.location.host;
|
||||
const { params } = this.currentRoute();
|
||||
const volumeName = this.spec?.volumeName;
|
||||
|
||||
if (!this.isLonghorn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!volumeName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,18 @@ export default class HciStorageClass extends StorageClass {
|
||||
return key ? this.$rootGetters['i18n/t'](key) : this.provisioner;
|
||||
}
|
||||
|
||||
get isEncrypted() {
|
||||
return this.parameters?.encrypted === 'true';
|
||||
}
|
||||
|
||||
get isLonghorn() {
|
||||
return this.provisioner === LONGHORN_DRIVER;
|
||||
}
|
||||
|
||||
get isLonghornV1() {
|
||||
return this.provisioner === LONGHORN_DRIVER && this.longhornVersion === DATA_ENGINE_V1;
|
||||
}
|
||||
|
||||
get isLonghornV2() {
|
||||
return this.provisioner === LONGHORN_DRIVER && this.longhornVersion === DATA_ENGINE_V2;
|
||||
}
|
||||
@@ -69,4 +81,8 @@ export default class HciStorageClass extends StorageClass {
|
||||
get volumeEncryptionFeatureEnabled() {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
|
||||
}
|
||||
|
||||
get thirdPartyStorageFeatureEnabled() {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,17 +28,15 @@ export default class HciSetting extends HarvesterResource {
|
||||
}
|
||||
|
||||
const schema = this.$getters['schemaFor'](HCI.UPGRADE);
|
||||
|
||||
const hasUpgradeAccess = !!schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase()));
|
||||
|
||||
if (this.id === HCI_SETTING.SERVER_VERSION && hasUpgradeAccess) {
|
||||
const latestUpgrade = this.$getters['all'](HCI.UPGRADE).find((upgrade) => upgrade.isLatestUpgrade);
|
||||
|
||||
out.unshift({
|
||||
action: 'goToAirgapUpgrade',
|
||||
enabled: true,
|
||||
icon: 'icon icon-refresh',
|
||||
label: this.t('harvester.upgradePage.upgrade'),
|
||||
disabled: !!latestUpgrade && !latestUpgrade?.isUpgradeSucceeded
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ export default class HciVmBackup extends HarvesterResource {
|
||||
}
|
||||
|
||||
get sourceSchedule() {
|
||||
return this.metadata?.annotations[HCI_ANNOTATIONS.SVM_BACKUP_ID];
|
||||
return this.metadata?.annotations?.[HCI_ANNOTATIONS.SVM_BACKUP_ID];
|
||||
}
|
||||
|
||||
get attachVM() {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { HCI } from '../types';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
||||
import HarvesterResource from './harvester';
|
||||
import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map';
|
||||
import { UNIT_SUFFIX } from '../utils/unit';
|
||||
|
||||
const {
|
||||
CSI_PROVISIONER_SECRET_NAME,
|
||||
@@ -49,35 +50,45 @@ export default class HciVmImage extends HarvesterResource {
|
||||
canCreateVM = false;
|
||||
}
|
||||
|
||||
return [
|
||||
const customActions = this.isReady ? [
|
||||
{
|
||||
action: 'createFromImage',
|
||||
enabled: canCreateVM,
|
||||
icon: 'icon icon-circle-plus',
|
||||
label: this.t('harvester.action.createVM'),
|
||||
disabled: !this.isReady,
|
||||
},
|
||||
{
|
||||
action: 'encryptImage',
|
||||
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted,
|
||||
icon: 'icon icon-lock',
|
||||
label: this.t('harvester.action.encryptImage'),
|
||||
disabled: !this.isReady,
|
||||
},
|
||||
{
|
||||
action: 'decryptImage',
|
||||
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted,
|
||||
icon: 'icon icon-unlock',
|
||||
label: this.t('harvester.action.decryptImage'),
|
||||
disabled: !this.isReady,
|
||||
},
|
||||
{
|
||||
action: 'download',
|
||||
enabled: this.links?.download,
|
||||
icon: 'icon icon-download',
|
||||
label: this.t('asyncButton.download.action'),
|
||||
},
|
||||
...out
|
||||
action: 'imageDownload',
|
||||
enabled: this.links?.download,
|
||||
icon: 'icon icon-download',
|
||||
label: this.t('asyncButton.download.action'),
|
||||
}
|
||||
] : [];
|
||||
|
||||
let filteredOut;
|
||||
|
||||
if (customActions.length > 0) {
|
||||
filteredOut = out;
|
||||
} else {
|
||||
// if the first item is a divider, remove it from the array
|
||||
filteredOut = out[0]?.divider ? out.slice(1) : out;
|
||||
}
|
||||
|
||||
return [
|
||||
...customActions,
|
||||
...filteredOut
|
||||
];
|
||||
}
|
||||
|
||||
@@ -199,6 +210,10 @@ export default class HciVmImage extends HarvesterResource {
|
||||
return `${ this.metadata.namespace }/${ this.spec.displayName }`;
|
||||
}
|
||||
|
||||
get imageStorageClass() {
|
||||
return this?.metadata?.annotations?.[HCI_ANNOTATIONS.STORAGE_CLASS] || '';
|
||||
}
|
||||
|
||||
get imageMessage() {
|
||||
if (this.uploadError) {
|
||||
return ucFirst(this.uploadError);
|
||||
@@ -239,8 +254,8 @@ export default class HciVmImage extends HarvesterResource {
|
||||
return formatSi(size, {
|
||||
increment: 1024,
|
||||
maxPrecision: 2,
|
||||
suffix: 'B',
|
||||
firstSuffix: 'B',
|
||||
suffix: UNIT_SUFFIX,
|
||||
firstSuffix: UNIT_SUFFIX,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -254,8 +269,8 @@ export default class HciVmImage extends HarvesterResource {
|
||||
return formatSi(virtualSize, {
|
||||
increment: 1024,
|
||||
maxPrecision: 2,
|
||||
suffix: 'B',
|
||||
firstSuffix: 'B',
|
||||
suffix: UNIT_SUFFIX,
|
||||
firstSuffix: UNIT_SUFFIX,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -290,7 +305,7 @@ export default class HciVmImage extends HarvesterResource {
|
||||
}
|
||||
|
||||
get uploadImage() {
|
||||
return async(file) => {
|
||||
return async(file, opt = {}) => {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('chunk', file);
|
||||
@@ -298,22 +313,23 @@ export default class HciVmImage extends HarvesterResource {
|
||||
try {
|
||||
this.$ctx.commit('harvester-common/uploadStart', this.metadata.name, { root: true });
|
||||
|
||||
await this.doAction('upload', formData, {
|
||||
const result = await this.doAction('upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'File-Size': file.size,
|
||||
},
|
||||
params: { size: file.size },
|
||||
signal: opt.signal,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
this.$ctx.commit('harvester-common/uploadError', { name: this.name, message: err.message }, { root: true });
|
||||
|
||||
this.$ctx.commit('harvester-common/uploadEnd', this.metadata.name, { root: true });
|
||||
|
||||
return Promise.reject(err);
|
||||
throw err;
|
||||
} finally {
|
||||
this.$ctx.commit('harvester-common/uploadEnd', this.metadata.name, { root: true });
|
||||
}
|
||||
|
||||
this.$ctx.commit('harvester-common/uploadEnd', this.metadata.name, { root: true });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -385,7 +401,23 @@ export default class HciVmImage extends HarvesterResource {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
|
||||
}
|
||||
|
||||
download() {
|
||||
get thirdPartyStorageFeatureEnabled() {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
||||
}
|
||||
|
||||
imageDownload(resources = this) {
|
||||
// spec.backend is introduced in v1.5.0. If it's not set, it's an old image can be downloaded via link
|
||||
if (this.spec?.backend === 'cdi') {
|
||||
this.$dispatch('promptModal', {
|
||||
resources,
|
||||
component: 'HarvesterImageDownloader'
|
||||
});
|
||||
} else {
|
||||
this.downloadViaLink();
|
||||
}
|
||||
}
|
||||
|
||||
downloadViaLink() {
|
||||
window.location.href = this.links.download;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,6 +210,10 @@ export default class HciVmTemplateVersion extends HarvesterResource {
|
||||
await templateResource.save();
|
||||
}
|
||||
|
||||
get cpuPinningFeatureEnabled() {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('cpuPinning');
|
||||
}
|
||||
|
||||
get defaultVersion() {
|
||||
const templates = this.$rootGetters['harvester/all'](HCI.VM_TEMPLATE);
|
||||
const template = templates.find((T) => this.templateId === T.id);
|
||||
@@ -272,4 +276,12 @@ export default class HciVmTemplateVersion extends HarvesterResource {
|
||||
|
||||
this.spec.vm.spec.template.metadata['labels'] = { ...wasIgnored, ...val };
|
||||
}
|
||||
|
||||
get tpmPersistentStateFeatureEnabled() {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('tpmPersistentState');
|
||||
}
|
||||
|
||||
get efiPersistentStateFeatureEnabled() {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('efiPersistentState');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import SteveModel from '@shell/plugins/steve/steve-class';
|
||||
import { HCI } from '@shell/config/labels-annotations';
|
||||
import { NETWORK_TYPE } from '../config/types';
|
||||
|
||||
const { UNTAGGED } = NETWORK_TYPE;
|
||||
|
||||
export default class NetworkAttachmentDef extends SteveModel {
|
||||
applyDefaults() {
|
||||
@@ -42,7 +45,7 @@ export default class NetworkAttachmentDef extends SteveModel {
|
||||
}
|
||||
|
||||
get vlanId() {
|
||||
return this.vlanType === 'UntaggedNetwork' ? 'N/A' : this.parseConfig.vlan;
|
||||
return this.vlanType === UNTAGGED ? 'N/A' : this.parseConfig.vlan;
|
||||
}
|
||||
|
||||
get customValidationRules() {
|
||||
@@ -65,7 +68,7 @@ export default class NetworkAttachmentDef extends SteveModel {
|
||||
const route = annotations[HCI.NETWORK_ROUTE];
|
||||
let config = {};
|
||||
|
||||
if (this.vlanType === 'UntaggedNetwork') {
|
||||
if (this.vlanType === UNTAGGED) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
|
||||
@@ -102,11 +102,13 @@ export default class VirtVm extends HarvesterResource {
|
||||
|
||||
return [
|
||||
{
|
||||
action: 'stopVM',
|
||||
enabled: !!this.actions?.stop,
|
||||
icon: 'icon icon-close',
|
||||
label: this.t('harvester.action.stop'),
|
||||
bulkable: true
|
||||
action: 'stopVM',
|
||||
altAction: 'altStopVM',
|
||||
enabled: !!this.actions?.stop,
|
||||
icon: 'icon icon-close',
|
||||
label: this.t('harvester.action.stop'),
|
||||
bulkable: true,
|
||||
bulkAction: 'stopVM',
|
||||
},
|
||||
{
|
||||
action: 'forceStop',
|
||||
@@ -116,10 +118,11 @@ export default class VirtVm extends HarvesterResource {
|
||||
bulkable: true
|
||||
},
|
||||
{
|
||||
action: 'pauseVM',
|
||||
enabled: !!this.actions?.pause,
|
||||
icon: 'icon icon-pause',
|
||||
label: this.t('harvester.action.pause')
|
||||
action: 'pauseVM',
|
||||
altAction: 'altPauseVM',
|
||||
enabled: !!this.actions?.pause,
|
||||
icon: 'icon icon-pause',
|
||||
label: this.t('harvester.action.pause')
|
||||
},
|
||||
{
|
||||
action: 'unpauseVM',
|
||||
@@ -155,7 +158,7 @@ export default class VirtVm extends HarvesterResource {
|
||||
},
|
||||
{
|
||||
action: 'takeVMSnapshot',
|
||||
enabled: !!this.actions?.backup && !this.longhornV2Volumes.length,
|
||||
enabled: (!!this.actions?.snapshot || !!this.action?.backup) && !this.longhornV2Volumes.length,
|
||||
icon: 'icon icon-snapshot',
|
||||
label: this.t('harvester.action.vmSnapshot')
|
||||
},
|
||||
@@ -402,7 +405,16 @@ export default class VirtVm extends HarvesterResource {
|
||||
return node?.id;
|
||||
}
|
||||
|
||||
pauseVM() {
|
||||
pauseVM(resources = this) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources,
|
||||
action: 'pause',
|
||||
warningMessageKey: 'dialog.confirmExecution.pause.message',
|
||||
component: 'ConfirmExecutionDialog'
|
||||
});
|
||||
}
|
||||
|
||||
altPauseVM() {
|
||||
this.doActionGrowl('pause', {});
|
||||
}
|
||||
|
||||
@@ -417,7 +429,16 @@ export default class VirtVm extends HarvesterResource {
|
||||
this.doActionGrowl('unpause', {});
|
||||
}
|
||||
|
||||
stopVM() {
|
||||
stopVM(resources = this) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources,
|
||||
action: 'stop',
|
||||
warningMessageKey: 'dialog.confirmExecution.stop.message',
|
||||
component: 'ConfirmExecutionDialog'
|
||||
});
|
||||
}
|
||||
|
||||
altStopVM() {
|
||||
this.doActionGrowl('stop', {});
|
||||
}
|
||||
|
||||
@@ -619,6 +640,10 @@ export default class VirtVm extends HarvesterResource {
|
||||
}
|
||||
|
||||
get encryptedVolumeType() {
|
||||
if (!this.volumes || this.volumes.length === 0) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
if (this.volumes.every((vol) => vol.isEncrypted)) {
|
||||
return 'all';
|
||||
} else if (this.volumes.some((vol) => vol.isEncrypted)) {
|
||||
@@ -675,8 +700,25 @@ export default class VirtVm extends HarvesterResource {
|
||||
return null;
|
||||
}
|
||||
|
||||
get isBeingStopped() {
|
||||
if (this && !this.isVMExpectedRunning && this.isVMCreated && this.vmi?.status?.phase !== VMIPhase.Succeeded) {
|
||||
get isPending() {
|
||||
if (this &&
|
||||
!this.isVMExpectedRunning &&
|
||||
this.isVMCreated &&
|
||||
this.vmi?.status?.phase === VMIPhase.Pending
|
||||
) {
|
||||
return { status: VMIPhase.Pending };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
get isStopping() {
|
||||
if (this &&
|
||||
!this.isVMExpectedRunning &&
|
||||
this.isVMCreated &&
|
||||
this.vmi?.status?.phase !== VMIPhase.Succeeded &&
|
||||
this.vmi?.status?.phase !== VMIPhase.Pending
|
||||
) {
|
||||
return { status: STOPPING };
|
||||
}
|
||||
|
||||
@@ -711,7 +753,7 @@ export default class VirtVm extends HarvesterResource {
|
||||
}
|
||||
|
||||
get isUnschedulable() {
|
||||
if (this.isBeingStopped || this.isStarting) {
|
||||
if (this.isStopping || this.isStarting) {
|
||||
const condition = this.status?.conditions?.find((c) => c.reason === UNSCHEDULABLE);
|
||||
|
||||
if (!!condition) {
|
||||
@@ -827,7 +869,8 @@ export default class VirtVm extends HarvesterResource {
|
||||
this.isUnschedulable?.status ||
|
||||
this.isPaused?.status ||
|
||||
this.isVMError?.status ||
|
||||
this.isBeingStopped?.status ||
|
||||
this.isPending?.status ||
|
||||
this.isStopping?.status ||
|
||||
this.isOff?.status ||
|
||||
this.isError?.status ||
|
||||
this.isRunning?.status ||
|
||||
@@ -1150,6 +1193,18 @@ export default class VirtVm extends HarvesterResource {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
|
||||
}
|
||||
|
||||
get tpmPersistentStateFeatureEnabled() {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('tpmPersistentState');
|
||||
}
|
||||
|
||||
get efiPersistentStateFeatureEnabled() {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('efiPersistentState');
|
||||
}
|
||||
|
||||
get thirdPartyStorageFeatureEnabled() {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
||||
}
|
||||
|
||||
setInstanceLabels(val) {
|
||||
if ( !this.spec?.template?.metadata?.labels ) {
|
||||
set(this, 'spec.template.metadata.labels', {});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "harvester",
|
||||
"description": "Rancher UI Extension for Harvester",
|
||||
"version": "1.0.6",
|
||||
"version": "1.5.2-rc2",
|
||||
"private": false,
|
||||
"rancher": {
|
||||
"annotations": {
|
||||
"catalog.cattle.io/display-name": "Harvester",
|
||||
"catalog.cattle.io/kube-version": ">= 1.16.0-0",
|
||||
"catalog.cattle.io/rancher-version": ">= 2.10.1-0",
|
||||
"catalog.cattle.io/rancher-version": ">= 2.11.0-0",
|
||||
"catalog.cattle.io/ui-extensions-version": ">= 3.0.0 < 4.0.0"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,13 +7,16 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import UpgradeInfo from '../../../../components/UpgradeInfo';
|
||||
|
||||
import { HCI } from '../../../../types';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../../config/harvester';
|
||||
import ImagePercentageBar from '@shell/components/formatter/ImagePercentageBar';
|
||||
import { Banner } from '@components/Banner';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
|
||||
const IMAGE_METHOD = {
|
||||
NEW: 'new',
|
||||
EXIST: 'exist'
|
||||
NEW: 'new',
|
||||
EXIST: 'exist',
|
||||
DELETE: 'delete'
|
||||
};
|
||||
|
||||
const DOWNLOAD = 'download';
|
||||
@@ -22,7 +25,7 @@ const UPLOAD = 'upload';
|
||||
export default {
|
||||
name: 'HarvesterAirgapUpgrade',
|
||||
components: {
|
||||
Checkbox, CruResource, LabeledSelect, LabeledInput, RadioGroup, UpgradeInfo
|
||||
Checkbox, CruResource, LabeledSelect, LabeledInput, RadioGroup, UpgradeInfo, ImagePercentageBar, Banner
|
||||
},
|
||||
|
||||
inheritAttrs: false,
|
||||
@@ -39,36 +42,32 @@ export default {
|
||||
spec: { image: '' }
|
||||
});
|
||||
|
||||
const imageValue = await this.$store.dispatch('harvester/create', {
|
||||
type: HCI.IMAGE,
|
||||
metadata: {
|
||||
name: '',
|
||||
namespace: 'harvester-system',
|
||||
generateName: 'image-',
|
||||
annotations: {}
|
||||
},
|
||||
spec: {
|
||||
sourceType: UPLOAD,
|
||||
displayName: '',
|
||||
checksum: ''
|
||||
},
|
||||
});
|
||||
|
||||
await this.initImageValue();
|
||||
this.value = value;
|
||||
this.imageValue = imageValue;
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
if (this.uploadController) {
|
||||
this.uploadController.abort();
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
value: null,
|
||||
file: {},
|
||||
imageId: '',
|
||||
imageSource: IMAGE_METHOD.NEW,
|
||||
sourceType: UPLOAD,
|
||||
imageValue: null,
|
||||
errors: [],
|
||||
enableLogging: true,
|
||||
IMAGE_METHOD
|
||||
value: null,
|
||||
file: {},
|
||||
uploadImageId: '',
|
||||
imageId: '',
|
||||
deleteImageId: '',
|
||||
imageSource: IMAGE_METHOD.NEW,
|
||||
sourceType: UPLOAD,
|
||||
uploadController: null,
|
||||
uploadResult: null,
|
||||
imageValue: null,
|
||||
enableLogging: true,
|
||||
IMAGE_METHOD,
|
||||
skipSingleReplicaDetachedVol: false,
|
||||
errors: [],
|
||||
};
|
||||
},
|
||||
|
||||
@@ -77,22 +76,45 @@ export default {
|
||||
return `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||
},
|
||||
|
||||
osImageOptions() {
|
||||
return this.$store.getters['harvester/all'](HCI.IMAGE)
|
||||
.filter((I) => I.isOSImage)
|
||||
.map((I) => {
|
||||
return {
|
||||
label: I.spec.displayName,
|
||||
value: I.id,
|
||||
disabled: !I.isReady
|
||||
};
|
||||
});
|
||||
skipSingleReplicaDetachedVolFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
|
||||
},
|
||||
|
||||
uploadImage() {
|
||||
allOSImages() {
|
||||
return this.$store.getters['harvester/all'](HCI.IMAGE).filter((I) => I.isOSImage) || [];
|
||||
},
|
||||
|
||||
deleteOSImageOptions() {
|
||||
return this.allOSImages.map((I) => {
|
||||
return {
|
||||
label: I.spec.displayName,
|
||||
value: I.id,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
osImageOptions() {
|
||||
return this.allOSImages.map((I) => {
|
||||
return {
|
||||
label: I.spec.displayName,
|
||||
value: I.id,
|
||||
disabled: !I.isReady
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
createNewImage() {
|
||||
return this.imageSource === IMAGE_METHOD.NEW;
|
||||
},
|
||||
|
||||
selectExistImage() {
|
||||
return this.imageSource === IMAGE_METHOD.EXIST;
|
||||
},
|
||||
|
||||
deleteExistImage() {
|
||||
return this.imageSource === IMAGE_METHOD.DELETE;
|
||||
},
|
||||
|
||||
fileName() {
|
||||
return this.file?.name || '';
|
||||
},
|
||||
@@ -100,21 +122,89 @@ export default {
|
||||
canEnableLogging() {
|
||||
return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG);
|
||||
},
|
||||
|
||||
uploadProgress() {
|
||||
const image = this.$store.getters['harvester/byId'](HCI.IMAGE, this.imageValue.id);
|
||||
|
||||
return image?.status?.progress;
|
||||
},
|
||||
|
||||
enableUpgrade() {
|
||||
if (this.deleteExistImage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.sourceType === DOWNLOAD) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.sourceType === UPLOAD) {
|
||||
return this.fileName === '' ? true : this.uploadProgress === 100;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
isUploading() {
|
||||
return this.fileName !== '' && this.uploadProgress !== 100;
|
||||
},
|
||||
|
||||
showProgressBar() {
|
||||
return this.createNewImage && this.sourceType === UPLOAD && this.isUploading;
|
||||
},
|
||||
|
||||
showUploadSuccessBanner() {
|
||||
return this.createNewImage && this.fileName !== '' && isEmpty(this.errors) && !this.showUploadingWarningBanner && this.uploadResult?._status === 200;
|
||||
},
|
||||
|
||||
showUploadingWarningBanner() {
|
||||
return this.createNewImage && this.isUploading;
|
||||
},
|
||||
|
||||
showUpgradeOptions() {
|
||||
return this.createNewImage || this.selectExistImage;
|
||||
},
|
||||
|
||||
disableUploadButton() {
|
||||
return this.sourceType === UPLOAD && this.isUploading;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
done() {
|
||||
if (this.uploadController) {
|
||||
this.uploadController.abort();
|
||||
}
|
||||
this.$router.push({
|
||||
name: this.doneRoute,
|
||||
params: { resource: HCI.SETTING, product: 'harvester' }
|
||||
});
|
||||
},
|
||||
|
||||
async initImageValue() {
|
||||
this.imageValue = await this.$store.dispatch('harvester/create', {
|
||||
type: HCI.IMAGE,
|
||||
metadata: {
|
||||
name: '',
|
||||
namespace: 'harvester-system',
|
||||
generateName: 'image-',
|
||||
annotations: {}
|
||||
},
|
||||
spec: {
|
||||
sourceType: UPLOAD,
|
||||
displayName: '',
|
||||
checksum: this.imageValue?.spec?.checksum || '',
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
let res = null;
|
||||
|
||||
this.file = {};
|
||||
this.errors = [];
|
||||
if (!this.imageValue.spec.displayName && this.uploadImage) {
|
||||
|
||||
if (!this.imageValue.spec.displayName && this.createNewImage) {
|
||||
this.errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('generic.name') }));
|
||||
buttonCb(false);
|
||||
|
||||
@@ -125,26 +215,11 @@ export default {
|
||||
if (this.imageSource === IMAGE_METHOD.NEW) {
|
||||
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
|
||||
|
||||
if (this.sourceType === UPLOAD) {
|
||||
this.imageValue.spec.sourceType = UPLOAD;
|
||||
const file = this.file;
|
||||
|
||||
if (!file.name) {
|
||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.selectExitImage'));
|
||||
buttonCb(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.imageValue.spec.url = '';
|
||||
|
||||
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.IMAGE_NAME] = file.name;
|
||||
|
||||
res = await this.imageValue.save();
|
||||
|
||||
res.uploadImage(file);
|
||||
if (this.sourceType === UPLOAD && this.uploadImageId !== '') {
|
||||
this.value.spec.image = this.uploadImageId;
|
||||
} else if (this.sourceType === DOWNLOAD) {
|
||||
this.imageValue.spec.sourceType = DOWNLOAD;
|
||||
|
||||
if (!this.imageValue.spec.url) {
|
||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.imageUrl'));
|
||||
buttonCb(false);
|
||||
@@ -153,12 +228,12 @@ export default {
|
||||
}
|
||||
|
||||
res = await this.imageValue.save();
|
||||
this.value.spec.image = res.id;
|
||||
}
|
||||
|
||||
this.value.spec.image = res.id;
|
||||
} else if (this.imageSource === IMAGE_METHOD.EXIST) {
|
||||
if (!this.imageId) {
|
||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.chooseFile'));
|
||||
buttonCb(false);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -169,18 +244,87 @@ export default {
|
||||
if (this.canEnableLogging) {
|
||||
this.value.spec.logEnabled = this.enableLogging;
|
||||
}
|
||||
if (this.skipSingleReplicaDetachedVolFeatureEnabled) {
|
||||
this.value.metadata.annotations = { [HCI_ANNOTATIONS.SKIP_SINGLE_REPLICA_DETACHED_VOL]: JSON.stringify(this.skipSingleReplicaDetachedVol) };
|
||||
}
|
||||
|
||||
await this.value.save();
|
||||
this.done();
|
||||
buttonCb(true);
|
||||
} catch (e) {
|
||||
this.errors = exceptionToErrorsArray(e);
|
||||
this.errors = [e?.message] || exceptionToErrorsArray(e);
|
||||
buttonCb(false);
|
||||
}
|
||||
},
|
||||
|
||||
handleFileUpload() {
|
||||
this.file = this.$refs.file.files[0];
|
||||
async uploadFile(file) {
|
||||
const fileName = file.name;
|
||||
|
||||
if (!fileName) {
|
||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.unknownImageName'));
|
||||
|
||||
return;
|
||||
}
|
||||
const isDuplicatedFile = this.allOSImages.some((I) => I.spec.displayName === fileName);
|
||||
|
||||
if (isDuplicatedFile) {
|
||||
this.errors.push(this.$store.getters['i18n/t']('harvester.upgradePage.upload.duplicatedFile'));
|
||||
this.file = {};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.errors = [];
|
||||
this.imageValue.spec.sourceType = UPLOAD;
|
||||
this.imageValue.spec.displayName = fileName;
|
||||
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
|
||||
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.IMAGE_NAME] = fileName;
|
||||
this.imageValue.spec.url = '';
|
||||
|
||||
try {
|
||||
const res = await this.imageValue.save();
|
||||
|
||||
this.uploadImageId = res.id;
|
||||
this.uploadController = new AbortController();
|
||||
|
||||
const signal = this.uploadController.signal;
|
||||
|
||||
this.uploadResult = await res.uploadImage(file, { signal });
|
||||
} catch (e) {
|
||||
if (e?.code === 'ERR_NETWORK') {
|
||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.networkError'));
|
||||
} else if (e?.code === 'ERR_CANCELED') {
|
||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.cancelUpload'));
|
||||
} else {
|
||||
this.errors = exceptionToErrorsArray(e);
|
||||
}
|
||||
this.file = {};
|
||||
this.uploadImageId = '';
|
||||
}
|
||||
},
|
||||
|
||||
handleImageDelete(imageId) {
|
||||
const image = this.allOSImages.find((I) => I.id === imageId);
|
||||
|
||||
if (image) {
|
||||
this.$store.dispatch('harvester/promptModal', {
|
||||
resources: [image],
|
||||
component: 'ConfirmRelatedToRemoveDialog',
|
||||
needConfirmation: false,
|
||||
warningMessage: this.$store.getters['i18n/t']('harvester.modal.osImage.message', { name: image.displayName })
|
||||
});
|
||||
this.deleteImageId = '';
|
||||
}
|
||||
},
|
||||
|
||||
async handleFileUpload() {
|
||||
this.uploadImageId = '';
|
||||
this.errors = [];
|
||||
this.file = this.$refs.file?.files[0];
|
||||
if (this.file) {
|
||||
await this.initImageValue();
|
||||
await this.uploadFile(this.file);
|
||||
}
|
||||
},
|
||||
|
||||
selectFile() {
|
||||
@@ -190,21 +334,27 @@ export default {
|
||||
},
|
||||
|
||||
watch: {
|
||||
imageSource(neu) {
|
||||
if (neu !== IMAGE_METHOD.DELETE) {
|
||||
this.deleteImageId = '';
|
||||
}
|
||||
},
|
||||
|
||||
'imageValue.spec.url': {
|
||||
handler(neu) {
|
||||
const suffixName = neu?.split('/')?.pop();
|
||||
const splitName = suffixName?.split('.') || [];
|
||||
const fileSuffix = splitName?.pop()?.toLowerCase();
|
||||
|
||||
if (splitName.length > 1 && fileSuffix === 'iso' && !this.imageValue.spec.displayName) {
|
||||
if (splitName.length > 1 && fileSuffix === 'iso' && suffixName !== this.imageValue.spec.displayName) {
|
||||
this.imageValue.spec.displayName = suffixName;
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
|
||||
file(neu) {
|
||||
if (!this.imageValue.spec.displayName && neu.name) {
|
||||
// update name input if select new image
|
||||
if (neu.name && neu.name !== this.imageValue.spec.displayName) {
|
||||
this.imageValue.spec.displayName = neu.name;
|
||||
}
|
||||
}
|
||||
@@ -227,9 +377,11 @@ export default {
|
||||
:errors="errors"
|
||||
:can-yaml="false"
|
||||
finish-button-mode="upgrade"
|
||||
:validation-passed="enableUpgrade"
|
||||
:cancel-event="true"
|
||||
@finish="save"
|
||||
@cancel="done"
|
||||
@error="e=>errors = e"
|
||||
>
|
||||
<RadioGroup
|
||||
v-model:value="imageSource"
|
||||
@@ -238,18 +390,57 @@ export default {
|
||||
:options="[
|
||||
IMAGE_METHOD.NEW,
|
||||
IMAGE_METHOD.EXIST,
|
||||
IMAGE_METHOD.DELETE,
|
||||
]"
|
||||
:labels="[
|
||||
t('harvester.upgradePage.uploadNew'),
|
||||
t('harvester.upgradePage.selectExisting'),
|
||||
t('harvester.upgradePage.deleteExisting'),
|
||||
]"
|
||||
/>
|
||||
|
||||
<UpgradeInfo />
|
||||
<UpgradeInfo v-if="createNewImage || selectExistImage" />
|
||||
|
||||
<div v-if="uploadImage">
|
||||
<Banner
|
||||
v-if="showUploadSuccessBanner"
|
||||
color="success"
|
||||
class="mt-0 mb-30"
|
||||
:label="t('harvester.setting.upgrade.uploadSuccess', { name: file.name })"
|
||||
/>
|
||||
<Banner
|
||||
v-if="showUploadingWarningBanner"
|
||||
color="warning"
|
||||
class="mt-0 mb-30"
|
||||
:label="t('harvester.image.warning.osUpgrade.uploading', { name: file.name })"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="showUpgradeOptions"
|
||||
class="mt-10 mb-10"
|
||||
>
|
||||
<Checkbox
|
||||
v-if="canEnableLogging"
|
||||
v-model:value="enableLogging"
|
||||
class="check mb-20"
|
||||
type="checkbox"
|
||||
:label="t('harvester.upgradePage.enableLogging')"
|
||||
/>
|
||||
<div
|
||||
v-if="skipSingleReplicaDetachedVolFeatureEnabled"
|
||||
class="mb-20"
|
||||
>
|
||||
<Checkbox
|
||||
v-model:value="skipSingleReplicaDetachedVol"
|
||||
class="check"
|
||||
type="checkbox"
|
||||
:label="t('harvester.upgradePage.skipSingleReplicaDetachedVol')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="createNewImage">
|
||||
<LabeledInput
|
||||
v-model.trim="imageValue.spec.displayName"
|
||||
v-model:value.trim="imageValue.spec.displayName"
|
||||
class="mb-20"
|
||||
label-key="harvester.fields.name"
|
||||
required
|
||||
@@ -261,14 +452,6 @@ export default {
|
||||
label-key="harvester.setting.upgrade.checksum"
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
v-if="canEnableLogging"
|
||||
v-model:value="enableLogging"
|
||||
class="check mb-20"
|
||||
type="checkbox"
|
||||
:label="t('harvester.upgradePage.enableLogging')"
|
||||
/>
|
||||
|
||||
<RadioGroup
|
||||
v-model:value="sourceType"
|
||||
class="mb-20 image-group"
|
||||
@@ -285,7 +468,7 @@ export default {
|
||||
|
||||
<LabeledInput
|
||||
v-if="sourceType === 'download'"
|
||||
v-model.trim="imageValue.spec.url"
|
||||
v-model:value.trim="imageValue.spec.url"
|
||||
class="labeled-input--tooltip"
|
||||
required
|
||||
label-key="harvester.image.url"
|
||||
@@ -298,6 +481,7 @@ export default {
|
||||
<button
|
||||
type="button"
|
||||
class="btn role-primary"
|
||||
:disabled="disableUploadButton"
|
||||
@click="selectFile"
|
||||
>
|
||||
{{ t('harvester.image.uploadFile') }}
|
||||
@@ -318,28 +502,56 @@ export default {
|
||||
{{ fileName ? fileName : t('harvester.generic.noFileChosen') }}
|
||||
</span>
|
||||
</div>
|
||||
<ImagePercentageBar
|
||||
v-if="showProgressBar"
|
||||
class="mt-20"
|
||||
:value="uploadProgress"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<LabeledSelect
|
||||
v-else
|
||||
v-if="selectExistImage"
|
||||
v-model:value="imageId"
|
||||
:options="osImageOptions"
|
||||
required
|
||||
class="mb-20"
|
||||
label-key="harvester.fields.image"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="deleteExistImage"
|
||||
class="mt-20"
|
||||
>
|
||||
<Banner
|
||||
color="info"
|
||||
class="mt-10 mb-30"
|
||||
:label="t('harvester.upgradePage.deleteHeader')"
|
||||
/>
|
||||
<LabeledSelect
|
||||
v-model:value="deleteImageId"
|
||||
:options="deleteOSImageOptions"
|
||||
required
|
||||
class="mb-20"
|
||||
label-key="harvester.fields.image"
|
||||
@update:value="handleImageDelete"
|
||||
/>
|
||||
</div>
|
||||
</CruResource>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#air-gap {
|
||||
padding: 20px;
|
||||
|
||||
:deep() .image-group .radio-group {
|
||||
display: flex;
|
||||
.radio-container {
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
.parent {
|
||||
grid-template-columns:auto 40px;
|
||||
}
|
||||
.chooseFile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -16,14 +16,12 @@ export default {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
names: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
@@ -69,12 +67,6 @@ export default {
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
plusMore() {
|
||||
const remaining = this.toRemove.length - this.names.length;
|
||||
|
||||
return this.t('promptRemove.andOthers', { count: remaining });
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -143,7 +135,7 @@ export default {
|
||||
<div>
|
||||
<div class="mt-10">
|
||||
{{ t('promptRemove.attemptingToRemove', {type}) }}
|
||||
<span v-clean-html="resourceNames(names, plusMore, t)"></span>
|
||||
<span v-clean-html="resourceNames(names, t)"></span>
|
||||
|
||||
<div class="mt-10">
|
||||
{{ t('harvester.virtualMachine.promptRemove.title') }}
|
||||
|
||||
@@ -5,16 +5,33 @@ import { featureEnabled, getVersion } from '../utils/feature-flags';
|
||||
|
||||
const state = function() {
|
||||
return {
|
||||
latestBundleId: '',
|
||||
bundlePending: false,
|
||||
showBundleModal: false,
|
||||
bundlePercentage: 0,
|
||||
uploadingImages: [],
|
||||
uploadingImageError: {},
|
||||
// support bundle
|
||||
latestBundleId: '',
|
||||
bundlePending: false,
|
||||
showBundleModal: false,
|
||||
bundlePercentage: 0,
|
||||
uploadingImages: [],
|
||||
uploadingImageError: {},
|
||||
// download cdi image
|
||||
downloadImageId: '',
|
||||
downloadImageInProgress: false,
|
||||
isDownloadImageCancel: false,
|
||||
};
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
setDownloadImageId(state, id) {
|
||||
state.downloadImageId = id;
|
||||
},
|
||||
|
||||
setDownloadImageCancel(state, value) {
|
||||
state.isDownloadImageCancel = value;
|
||||
},
|
||||
|
||||
setDownloadImageInProgress(state, value) {
|
||||
state.downloadImageInProgress = value;
|
||||
},
|
||||
|
||||
setLatestBundleId(state, bundleId) {
|
||||
state.latestBundleId = bundleId;
|
||||
},
|
||||
@@ -51,6 +68,14 @@ const getters = {
|
||||
return state.latestBundleId;
|
||||
},
|
||||
|
||||
isDownloadImageCancel(state) {
|
||||
return state.isDownloadImageCancel;
|
||||
},
|
||||
|
||||
isDownloadImageInProgress(state) {
|
||||
return state.downloadImageInProgress;
|
||||
},
|
||||
|
||||
isBundlePending(state) {
|
||||
return state.bundlePending;
|
||||
},
|
||||
@@ -98,6 +123,70 @@ const getters = {
|
||||
};
|
||||
|
||||
const actions = {
|
||||
async downloadImageProgress({
|
||||
state, dispatch, commit, rootGetters
|
||||
}) {
|
||||
const parse = Parse(window.history.href);
|
||||
|
||||
const id = state.downloadImageId; // id is image_ns / image_name
|
||||
|
||||
let imageCrd = await dispatch(
|
||||
'harvester/find',
|
||||
{ type: HCI.VM_IMAGE_DOWNLOADER, id },
|
||||
{ root: true }
|
||||
);
|
||||
|
||||
await commit('setDownloadImageInProgress', true);
|
||||
|
||||
let count = 0;
|
||||
|
||||
const timer = setInterval(async() => {
|
||||
count = count + 1;
|
||||
if (count % 3 === 0) {
|
||||
// ws maybe disconnect, force to get the latest status
|
||||
imageCrd = await dispatch(
|
||||
'harvester/find',
|
||||
{
|
||||
type: HCI.VM_IMAGE_DOWNLOADER,
|
||||
id,
|
||||
opt: { force: true }
|
||||
},
|
||||
{ root: true }
|
||||
);
|
||||
}
|
||||
|
||||
// If is cancel, clear the timer
|
||||
if (state.isDownloadImageCancel === true) {
|
||||
clearInterval(timer);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// converting image status becomes ready
|
||||
if (imageCrd?.status?.status === 'Ready') {
|
||||
imageCrd = rootGetters['harvester/byId'](HCI.VM_IMAGE_DOWNLOADER, id);
|
||||
|
||||
setTimeout(() => {
|
||||
commit('setDownloadImageInProgress', false);
|
||||
dispatch('promptModal'); // bring undefined data will close the promptModal
|
||||
}, 600);
|
||||
|
||||
if (rootGetters['isMultiCluster']) {
|
||||
const clusterId = rootGetters['clusterId'];
|
||||
const prefix = `/k8s/clusters/${ clusterId }`;
|
||||
|
||||
window.location.href = `${ parse.origin }${ prefix }/v1/harvester/${ HCI.IMAGE }/${ id }/download`;
|
||||
} else {
|
||||
const link = `${ parse.origin }/v1/harvester/${ HCI.IMAGE }/${ id }/download`;
|
||||
|
||||
window.location.href = link;
|
||||
}
|
||||
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
async bundleProgress({
|
||||
state, dispatch, commit, rootGetters
|
||||
}) {
|
||||
@@ -117,7 +206,7 @@ const actions = {
|
||||
const timer = setInterval(async() => {
|
||||
count = count + 1;
|
||||
if (count % 3 === 0) {
|
||||
// ws mayby disconnect
|
||||
// ws maybe disconnect
|
||||
bundleCrd = await dispatch(
|
||||
'harvester/find',
|
||||
{
|
||||
|
||||
+53
-52
@@ -1,56 +1,57 @@
|
||||
export const HCI = {
|
||||
VM: 'kubevirt.io.virtualmachine',
|
||||
VMI: 'kubevirt.io.virtualmachineinstance',
|
||||
VMIM: 'kubevirt.io.virtualmachineinstancemigration',
|
||||
VM_TEMPLATE: 'harvesterhci.io.virtualmachinetemplate',
|
||||
VM_VERSION: 'harvesterhci.io.virtualmachinetemplateversion',
|
||||
IMAGE: 'harvesterhci.io.virtualmachineimage',
|
||||
SSH: 'harvesterhci.io.keypair',
|
||||
VOLUME: 'harvesterhci.io.volume',
|
||||
USER: 'harvesterhci.io.user',
|
||||
SETTING: 'harvesterhci.io.setting',
|
||||
UPGRADE: 'harvesterhci.io.upgrade',
|
||||
UPGRADE_LOG: 'harvesterhci.io.upgradelog',
|
||||
SCHEDULE_VM_BACKUP: 'harvesterhci.io.schedulevmbackup',
|
||||
BACKUP: 'harvesterhci.io.virtualmachinebackup',
|
||||
RESTORE: 'harvesterhci.io.virtualmachinerestore',
|
||||
NODE_NETWORK: 'network.harvesterhci.io.nodenetwork',
|
||||
CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork',
|
||||
SUPPORT_BUNDLE: 'harvesterhci.io.supportbundle',
|
||||
NETWORK_ATTACHMENT: 'harvesterhci.io.networkattachmentdefinition',
|
||||
CLUSTER: 'harvesterhci.io.management.cluster',
|
||||
DASHBOARD: 'harvesterhci.io.dashboard',
|
||||
BLOCK_DEVICE: 'harvesterhci.io.blockdevice',
|
||||
CLOUD_TEMPLATE: 'harvesterhci.io.cloudtemplate',
|
||||
HOST: 'harvesterhci.io.host',
|
||||
VERSION: 'harvesterhci.io.version',
|
||||
SNAPSHOT: 'harvesterhci.io.volumesnapshot',
|
||||
VM_SNAPSHOT: 'harvesterhci.io.vmsnapshot',
|
||||
ALERTMANAGERCONFIG: 'harvesterhci.io.monitoring.alertmanagerconfig',
|
||||
CLUSTER_FLOW: 'harvesterhci.io.logging.clusterflow',
|
||||
CLUSTER_OUTPUT: 'harvesterhci.io.logging.clusteroutput',
|
||||
FLOW: 'harvesterhci.io.logging.flow',
|
||||
OUTPUT: 'harvesterhci.io.logging.output',
|
||||
STORAGE: 'harvesterhci.io.storage',
|
||||
RESOURCE_QUOTA: 'harvesterhci.io.resourcequota',
|
||||
KSTUNED: 'node.harvesterhci.io.ksmtuned',
|
||||
PCI_DEVICE: 'devices.harvesterhci.io.pcidevice',
|
||||
PCI_CLAIM: 'devices.harvesterhci.io.pcideviceclaim',
|
||||
SR_IOV: 'devices.harvesterhci.io.sriovnetworkdevice',
|
||||
VGPU_DEVICE: 'devices.harvesterhci.io.vgpudevice',
|
||||
SR_IOVGPU_DEVICE: 'devices.harvesterhci.io.sriovgpudevice',
|
||||
USB_DEVICE: 'devices.harvesterhci.io.usbdevice',
|
||||
USB_CLAIM: 'devices.harvesterhci.io.usbdeviceclaim',
|
||||
VLAN_CONFIG: 'network.harvesterhci.io.vlanconfig',
|
||||
VLAN_STATUS: 'network.harvesterhci.io.vlanstatus',
|
||||
ADD_ONS: 'harvesterhci.io.addon',
|
||||
LINK_MONITOR: 'network.harvesterhci.io.linkmonitor',
|
||||
SECRET: 'harvesterhci.io.secret',
|
||||
INVENTORY: 'metal.harvesterhci.io.inventory',
|
||||
LB: 'loadbalancer.harvesterhci.io.loadbalancer',
|
||||
IP_POOL: 'loadbalancer.harvesterhci.io.ippool',
|
||||
HARVESTER_CONFIG: 'rke-machine-config.cattle.io.harvesterconfig',
|
||||
LVM_VOLUME_GROUP: 'harvesterhci.io.lvmvolumegroup'
|
||||
VM: 'kubevirt.io.virtualmachine',
|
||||
VMI: 'kubevirt.io.virtualmachineinstance',
|
||||
VMIM: 'kubevirt.io.virtualmachineinstancemigration',
|
||||
VM_TEMPLATE: 'harvesterhci.io.virtualmachinetemplate',
|
||||
VM_VERSION: 'harvesterhci.io.virtualmachinetemplateversion',
|
||||
IMAGE: 'harvesterhci.io.virtualmachineimage',
|
||||
SSH: 'harvesterhci.io.keypair',
|
||||
VOLUME: 'harvesterhci.io.volume',
|
||||
USER: 'harvesterhci.io.user',
|
||||
SETTING: 'harvesterhci.io.setting',
|
||||
UPGRADE: 'harvesterhci.io.upgrade',
|
||||
UPGRADE_LOG: 'harvesterhci.io.upgradelog',
|
||||
SCHEDULE_VM_BACKUP: 'harvesterhci.io.schedulevmbackup',
|
||||
BACKUP: 'harvesterhci.io.virtualmachinebackup',
|
||||
RESTORE: 'harvesterhci.io.virtualmachinerestore',
|
||||
NODE_NETWORK: 'network.harvesterhci.io.nodenetwork',
|
||||
CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork',
|
||||
VM_IMAGE_DOWNLOADER: 'harvesterhci.io.virtualmachineimagedownloader',
|
||||
SUPPORT_BUNDLE: 'harvesterhci.io.supportbundle',
|
||||
NETWORK_ATTACHMENT: 'harvesterhci.io.networkattachmentdefinition',
|
||||
CLUSTER: 'harvesterhci.io.management.cluster',
|
||||
DASHBOARD: 'harvesterhci.io.dashboard',
|
||||
BLOCK_DEVICE: 'harvesterhci.io.blockdevice',
|
||||
CLOUD_TEMPLATE: 'harvesterhci.io.cloudtemplate',
|
||||
HOST: 'harvesterhci.io.host',
|
||||
VERSION: 'harvesterhci.io.version',
|
||||
SNAPSHOT: 'harvesterhci.io.volumesnapshot',
|
||||
VM_SNAPSHOT: 'harvesterhci.io.vmsnapshot',
|
||||
ALERTMANAGERCONFIG: 'harvesterhci.io.monitoring.alertmanagerconfig',
|
||||
CLUSTER_FLOW: 'harvesterhci.io.logging.clusterflow',
|
||||
CLUSTER_OUTPUT: 'harvesterhci.io.logging.clusteroutput',
|
||||
FLOW: 'harvesterhci.io.logging.flow',
|
||||
OUTPUT: 'harvesterhci.io.logging.output',
|
||||
STORAGE: 'harvesterhci.io.storage',
|
||||
RESOURCE_QUOTA: 'harvesterhci.io.resourcequota',
|
||||
KSTUNED: 'node.harvesterhci.io.ksmtuned',
|
||||
PCI_DEVICE: 'devices.harvesterhci.io.pcidevice',
|
||||
PCI_CLAIM: 'devices.harvesterhci.io.pcideviceclaim',
|
||||
SR_IOV: 'devices.harvesterhci.io.sriovnetworkdevice',
|
||||
VGPU_DEVICE: 'devices.harvesterhci.io.vgpudevice',
|
||||
SR_IOVGPU_DEVICE: 'devices.harvesterhci.io.sriovgpudevice',
|
||||
USB_DEVICE: 'devices.harvesterhci.io.usbdevice',
|
||||
USB_CLAIM: 'devices.harvesterhci.io.usbdeviceclaim',
|
||||
VLAN_CONFIG: 'network.harvesterhci.io.vlanconfig',
|
||||
VLAN_STATUS: 'network.harvesterhci.io.vlanstatus',
|
||||
ADD_ONS: 'harvesterhci.io.addon',
|
||||
LINK_MONITOR: 'network.harvesterhci.io.linkmonitor',
|
||||
SECRET: 'harvesterhci.io.secret',
|
||||
INVENTORY: 'metal.harvesterhci.io.inventory',
|
||||
LB: 'loadbalancer.harvesterhci.io.loadbalancer',
|
||||
IP_POOL: 'loadbalancer.harvesterhci.io.ippool',
|
||||
HARVESTER_CONFIG: 'rke-machine-config.cattle.io.harvesterconfig',
|
||||
LVM_VOLUME_GROUP: 'harvesterhci.io.lvmvolumegroup'
|
||||
};
|
||||
|
||||
export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot';
|
||||
|
||||
@@ -8,7 +8,6 @@ export const docLink = (suffix, version) => {
|
||||
};
|
||||
|
||||
export function getVersion(v) {
|
||||
// e.g v1.4.0
|
||||
if (process.env.VUE_APP_SERVER_VERSION) {
|
||||
return process.env.VUE_APP_SERVER_VERSION;
|
||||
}
|
||||
@@ -18,30 +17,45 @@ export function getVersion(v) {
|
||||
return `v${ semver.major(v) }.${ semver.minor(v) }.${ semver.patch(v) }`;
|
||||
} catch (error) {
|
||||
// fallback to the latest version
|
||||
return latestVersion(Object.keys(RELEASE_FEATURES));
|
||||
return getLatestVersion();
|
||||
}
|
||||
}
|
||||
|
||||
function latestVersion(versions) {
|
||||
return versions.sort((a, b) => semver.compare(a, b)).pop();
|
||||
function getLatestVersion() {
|
||||
const allVersions = Object.keys(RELEASE_FEATURES).filter(semver.valid).sort(semver.rcompare);
|
||||
|
||||
return allVersions[0] || '';
|
||||
}
|
||||
|
||||
// v1.3.3 => latest v1.3.x, v1.4.2 => latest v1.4.x
|
||||
function latestMinorVersion(v) {
|
||||
const minor = `v${ semver.major(v) }.${ semver.minor(v) }`;
|
||||
const minorVersions = Object.keys(RELEASE_FEATURES).filter((version) => version.startsWith(minor));
|
||||
function getLatestCompatibleVersion(version) {
|
||||
const allVersions = Object.keys(RELEASE_FEATURES).filter(semver.valid);
|
||||
const compatible = allVersions.filter((v) => semver.lte(v, version)).sort(semver.rcompare);
|
||||
|
||||
return latestVersion(minorVersions);
|
||||
return compatible[0] || getLatestVersion();
|
||||
}
|
||||
|
||||
export const featureEnabled = (featureKey, serverVersion) => {
|
||||
const minSupportedVersion = '1.3.0';
|
||||
|
||||
const version = getVersion(serverVersion);
|
||||
let releasedFeatures = RELEASE_FEATURES[version];
|
||||
|
||||
if (!releasedFeatures) {
|
||||
const fallback = latestMinorVersion(version);
|
||||
if (semver.lt(version.replace('v', ''), minSupportedVersion)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Harvester UI extension only supports Harvester cluster version >= ${ minSupportedVersion }. Current version: ${ version }`);
|
||||
|
||||
releasedFeatures = RELEASE_FEATURES[fallback];
|
||||
return false;
|
||||
}
|
||||
|
||||
const compatibleVersion = getLatestCompatibleVersion(version);
|
||||
const releasedFeatures = RELEASE_FEATURES?.[compatibleVersion];
|
||||
|
||||
if (!Array.isArray(releasedFeatures)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Feature flags for version ${ version } are not defined. Please upgrade Harvester UI extension and check the support matrix.`
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return releasedFeatures.includes(featureKey);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const UNIT_SUFFIX = 'i';
|
||||
export const GIBIBYTE = 'Gi';
|
||||
export const MEBIBYTE = 'Mi';
|
||||
@@ -2,6 +2,7 @@ import { PVC } from '@shell/config/types';
|
||||
import { isValidMac, isValidDNSLabelName } from '@pkg/utils/regular';
|
||||
import { SOURCE_TYPE } from '@pkg/config/harvester-map';
|
||||
import { parseVolumeClaimTemplates } from '@pkg/utils/vm';
|
||||
import { GIBIBYTE } from '../utils/unit';
|
||||
|
||||
const maxNameLength = 63;
|
||||
|
||||
@@ -88,7 +89,7 @@ export function vmDisks(spec, getters, errors, validatorArgs, displayKey, value)
|
||||
}
|
||||
|
||||
if (typeValue?.spec?.resources?.requests?.storage && !/^([0-9][0-9]{0,8})[a-zA-Z]+$/.test(typeValue?.spec?.resources?.requests?.storage)) {
|
||||
const message = getters['i18n/t']('harvester.validation.generic.maximumSize', { max: '999999999 GiB' });
|
||||
const message = getters['i18n/t']('harvester.validation.generic.maximumSize', { max: `999999999 ${ GIBIBYTE }` });
|
||||
|
||||
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { GIBIBYTE } from '../utils/unit';
|
||||
|
||||
export function volumeSize(size, getters, errors, validatorArgs, displayKey, value) {
|
||||
if (!size) {
|
||||
const key = getters['i18n/t']('harvester.volume.size');
|
||||
@@ -6,7 +8,7 @@ export function volumeSize(size, getters, errors, validatorArgs, displayKey, val
|
||||
}
|
||||
|
||||
if (size && !/^([0-9][0-9]{0,8})[a-zA-Z]+$/.test(size)) {
|
||||
const message = getters['i18n/t']('harvester.validation.generic.maximumSize', { max: '999999999 GiB' });
|
||||
const message = getters['i18n/t']('harvester.validation.generic.maximumSize', { max: `999999999 ${ GIBIBYTE }` });
|
||||
|
||||
errors.push(message);
|
||||
}
|
||||
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
TAG_VERSION=$1
|
||||
|
||||
if [[ -z "$TAG_VERSION" ]]; then
|
||||
echo "No tag version provided"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$(jq -r .version package.json)
|
||||
EXTENSION_VERSION=$(jq -r .version pkg/harvester/package.json)
|
||||
PKG_VERSION="v${EXTENSION_VERSION}"
|
||||
|
||||
if [[ "$VERSION" != "$EXTENSION_VERSION" ]]; then
|
||||
echo "Package version mismatch: $VERSION vs $EXTENSION_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${TAG_VERSION}" != "$PKG_VERSION" ]]; then
|
||||
echo "Package version $PKG_VERSION in pkg/harvester/package.json does not match tag version ${TAG_VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Package version and tag version match: ${TAG_VERSION}"
|
||||
Reference in New Issue
Block a user