Compare commits

..
83 Commits
Author SHA1 Message Date
mergify[bot]andAndy Lee b8b4ee4c41 replace tag format v1.5.0 to harvester-1.5.0 (#190) (#313)
(cherry picked from commit 374b904191)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-05-26 14:21:47 +08:00
Andy Lee 021d6f673f bump version to 1.0.6
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-26 13:48:57 +08:00
Andy Lee a849671034 bump to v1.0.6-rc4
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-23 11:19:39 +08:00
mergify[bot]andYiya Chen b98f878c5d fix: range condition (#299) (#300)
(cherry picked from commit cbfcf4dbae)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-05-15 16:26:48 +08:00
mergify[bot]andJack Yu 78482285b3 chore: use default imag repo name (#212) (#291)
(cherry picked from commit de4edbbf3b)

Signed-off-by: Jack Yu <jack.yu@suse.com>
Co-authored-by: Jack Yu <jack.yu@suse.com>
2025-05-13 12:10:05 +08:00
mergify[bot]andJack Yu 4656976b91 chore: add building catalog GA (#181) (#290)
* chore: add building catalog GA



* feat: should include prerelease



---------


(cherry picked from commit 1880043a80)

Signed-off-by: Jack Yu <jack.yu@suse.com>
Co-authored-by: Jack Yu <jack.yu@suse.com>
2025-05-13 12:08:06 +08:00
renovate[bot] aeb3588b83 chore(deps): update dependency follow-redirects to v1.15.6 [security] (#287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-13 04:07:25 +00:00
renovate[bot] d14b0cb95c chore(deps): update dependency ejs to v3.1.10 [security] (#286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-13 03:10:36 +00:00
Andy Lee 2f0884d7bd bump to v1.0.6-rc3
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-13 11:07:05 +08:00
mergify[bot]andAndy Lee 037ce6132b VM Access Credentials SSH Keys Not Rendering (backport #279) (#281)
* Remove unneeded condition (#279)

Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 8410f75859)

# Conflicts:
#	pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineSSHKey.vue

* solve conflict

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-05-12 10:56:49 +08:00
Andy Lee 47b54b4f06 bump version to 1.0.6-rc2
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-08 17:35:08 +08:00
Andy Lee 9dd8ae479a fix #cloud-config disappear when add/remove ssh key (#273)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-08 17:32:35 +08:00
Andy Lee 5682bf4c3a Fix create ssh key modal not pop up in rancher integration mode (#269)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-07 15:50:29 +08:00
Andy Lee 960df31667 Bump version to 1.0.6-rc1
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-06 11:57:48 +08:00
Yi-Ya Chen 63451e7d2b ci: rename file
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-06 11:56:30 +08:00
Yi-Ya Chen 55aaa8440d ci: revert change
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-06 11:56:29 +08:00
Yi-Ya Chen da4121da69 ci: rename the workflow 2025-05-06 11:56:29 +08:00
Yi-Ya Chen 30e49a6f2c ci: add ci pipeline
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-06 11:56:28 +08:00
Andy Lee 2255bd4f69 Add 1.4.3 feature flag
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-06 11:26:53 +08:00
Andy Lee 7e5ee04637 fix template page should be groupable (#262)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-17 15:45:40 +08:00
mergify[bot]andAndy Lee 8863de8874 fix the doneOverride path for harvesterhci.io.virtualmachinetemplate (#211) (#261)
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 22a032e56c)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-04-17 15:40:34 +08:00
mergify[bot]andAndy Lee 73952a6b4e update RadioGroup update value action (#255) (#259)
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 74300f24b3)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-04-17 15:29:29 +08:00
mergify[bot]andAndy Lee 0496fae623 fix percent typo (#258)
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 487f9abc10)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-04-17 15:25:00 +08:00
mergify[bot]andandy.lee d5cd97aafd Fix upgrade cluster model styles (backport #162) (#257)
* fix upgrade model style

Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit ff439e66a9)

* change overallMessage color to red

Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 50901a2eaf)

---------

Co-authored-by: andy.lee <andy.lee@suse.com>
2025-04-17 15:23:32 +08:00
mergify[bot]andYi-Ya Chen 31c6a794be fix: register upgrade component in header (#256)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
(cherry picked from commit b499f62c56)

Co-authored-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-04-17 15:19:08 +08:00
mergify[bot]andandy.lee 9d9383ba90 fix: vGPU number not correct (#254)
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 26afffc2f1)
(cherry picked from commit 40dc31226b)

Co-authored-by: andy.lee <andy.lee@suse.com>
2025-04-17 14:56:05 +08:00
mergify[bot]andandy.lee 1e02dbffe3 fix: isEncrypted breaks all the volume page (#253)
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 2232e56ba1)
(cherry picked from commit c188f3367d)

Co-authored-by: andy.lee <andy.lee@suse.com>
2025-04-17 14:50:31 +08:00
mergify[bot]andandy.lee b0609020cc fix sshkey is not added to cloud init user data immediately (#252)
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit e2daa1ea0c)

Co-authored-by: andy.lee <andy.lee@suse.com>
2025-04-17 14:50:10 +08:00
mergify[bot]andAndy Lee feca476e36 Fix error banner position in all dialogs (backport #226) (#250)
* Fix error banner position in all dialogs (#226)

* fix error banner position in all dialogs

Signed-off-by: Andy Lee <andy.lee@suse.com>

* update HarvesterImageDownloader.vue

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 25439aee65)

# Conflicts:
#	pkg/harvester/dialog/ConfirmExecutionDialog.vue
#	pkg/harvester/dialog/HarvesterImageDownloader.vue

* remove unneeded dialog components

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-04-17 14:46:30 +08:00
mergify[bot]andAndy Lee e6e36e94f1 fix image.repo to image.repository (#214) (#251)
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit a24d01717a)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-04-17 14:46:11 +08:00
Andy Lee 54d48ce396 bump version to 1.0.6
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-17 13:47:13 +08:00
Andy Lee 404395c7d9 bump version to 1.0.5
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-08 15:26:18 +08:00
Andy Lee dc2437db99 bring status object in PUT dismiss API
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-08 15:12:13 +08:00
Andy Lee e8c8a54b9f add cleanForSave in harvester model
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-08 15:12:07 +08:00
andy.lee f3e4a8afe8 add pcideviceclaim and usbdeviceclaim models
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-04-08 15:11:55 +08:00
andy.lee e73c62739f fix unable to add/delete new hard disk
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-04-08 15:03:14 +08:00
Andy Lee 696af4e04f update yarn.lock
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-12 12:05:24 +08:00
Yi-Ya Chen 009f384a73 feat: fix $ctx redefine issue
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-03-12 12:04:24 +08:00
andy.lee 37766454bd move harvester schema.js into harvester folder
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-03-12 12:02:36 +08:00
Andy Lee 7943913ef7 replace tag format v1.5.0 to harvester-1.5.0 (#190)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-12 12:02:11 +08:00
Andy Lee 56a0fefd40 bump release version to v1.0.4
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-11 12:11:37 +08:00
Andy Lee ff1d5925a8 remove require dot for non-longhorn v1
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-11 12:09:18 +08:00
Andy Lee 63b5b980b7 Fix infobox style
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-11 12:09:10 +08:00
Yi-Ya Chen 1f7fd1246f refactor: default should selectable
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-03-11 12:07:52 +08:00
Yi-Ya Chen 34a25e9fc8 feat: gray out class name selector
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-03-11 12:07:32 +08:00
Andy Lee fb8d81d953 bump version to v1.0.4-rc2 2025-03-11 12:06:29 +08:00
andy.lee ba810a52b2 add refreshIntervalInSecond in 1.4.2 feature flag
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-03-11 12:06:29 +08:00
andy.lee 3f4f30ba0b Add refreshIntervalInSeconds in backup targe edit page
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-03-11 12:06:29 +08:00
andy.lee b428aaecf8 bump version to v1.0.4-rc1
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-13 14:34:14 +08:00
andy.lee 6b07a03451 add v1.4.2 feature flag
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-13 14:19:37 +08:00
Volker Theile 2c756582e3 The hints on the branding page is using the wrong product name
The hints on the `Branding` page at `Advanced > Settings > UI > branding` are using the product name `Rancher` instead of `Harvester`.

Related to: https://github.com/harvester/harvester/issues/6341

Signed-off-by: Volker Theile <vtheile@suse.com>
(cherry picked from commit 849dcb6f26d52c955db669207f008de4480fe048)
2025-02-13 14:11:24 +08:00
Andy Lee 1c15ed8003 Merge pull request #92 from a110605/bump_v103_release
chore: bump release version to v1.0.3
2025-01-17 09:57:49 +08:00
Andy Lee 9f3e689727 Merge pull request #91 from a110605/issue_7351
fix: bring default valueContext when customizing vm-import controller
2025-01-17 09:56:58 +08:00
andy.lee 6644651fe5 chore: bump release version to v1.0.3
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-16 14:03:46 +08:00
andy.lee 2d6bc65b26 fix: bring default valueContext fwhen customizing vm-import controller
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-16 12:18:57 +08:00
Francesco Torchia 7b6efa624f Merge pull request #86 from torchiaf/replace-sortablejs-vue3
Replace sortablejs-vue3
2025-01-14 17:02:21 +01:00
Francesco Torchia d9db1263d1 Fix Volumes draggable key
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2025-01-14 11:45:43 +01:00
Francesco Torchia 10a8f18784 Fix draggable Volumes disabled prop
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2025-01-14 10:09:30 +01:00
Andy Lee 1098ef8f6a Merge pull request #84 from a110605/issue_7174_2
fix: resume schedule job error
2025-01-14 12:55:41 +08:00
Andy Lee 4480dbdeed Merge pull request #85 from a110605/issue_7347
fix: filter schedule style and functionality
2025-01-14 12:55:24 +08:00
Francesco Torchia a7b4366d7c Replace sortablejs-vue3
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2025-01-13 18:13:18 +01:00
andy.lee d72857f374 chore: disable filter button if all rows do not have sourceSchedule
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-13 16:29:08 +08:00
andy.lee 57ab9e2830 fix: filter schedule style and functionality
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-13 16:07:32 +08:00
andy.lee 8116358011 fix: resume schedule job error
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-13 14:13:00 +08:00
Andy Lee 5bd8ad2a93 Merge pull request #79 from a110605/fix_7317
fix: add missing wording in delete VM modal
2025-01-13 13:36:52 +08:00
Andy Lee 345fe636ec Merge pull request #72 from torchiaf/7082-host-vms
[porting 1262] Fix vm rows reactivity
2025-01-13 11:58:45 +08:00
andy.lee 7e32ea453b fix: delete VM modal style and wording
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-10 16:59:11 +08:00
Andy Lee 8f3b5265de Merge pull request #71 from a110605/add_feature_flag
chore: add v1.5.0 and v1.3.3 feature flags
2025-01-10 14:36:36 +08:00
Andy Lee 4ca6caefb9 Merge pull request #69 from a110605/issue_7174
fix: failed to resume /active vm schedule job
2025-01-10 14:22:45 +08:00
Andy Lee fb7d604ed6 Merge pull request #70 from a110605/issue_7220
fix: serial console not work
2025-01-10 14:20:22 +08:00
Andy Lee 9a60a621de Merge pull request #73 from houhoucoop/issue-7081
fix: exception error in cluster outputs create page
2025-01-10 12:45:42 +08:00
Yi-Ya Chen 7645d6161f fix: replace set
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-10 10:54:58 +08:00
Francesco Torchia c3f2665912 [porting 1262] Fix vm rows reactivity
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2025-01-09 12:06:35 +01:00
andy.lee 2a58745550 chore: add v1.5.0 and v1.3.3 feature flags
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-09 17:43:19 +08:00
andy.lee 69d7046bc7 fix: serial console not work
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-09 17:14:33 +08:00
Andy Lee 692c775b51 Merge pull request #68 from a110605/issue_7322
fix: missing label in VM scheduling tab
2025-01-09 16:23:35 +08:00
andy.lee 8b43c200b2 fix: remove translation key
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-09 16:13:51 +08:00
andy.lee ab71cb377c fix: update translation key
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-09 10:59:49 +08:00
andy.lee 814d296d75 fix: resume/active scheduling job failed
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-08 15:59:25 +08:00
andy.lee eb667fd56b fix: missing label in VM scheduling tab
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-08 14:41:18 +08:00
andy.lee e4b3f84905 change mergify.yml base branch to main
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 21:37:38 +08:00
Andy Lee ea68b95c26 Merge pull request #67 from a110605/add_mergify
add mergify.yml
2025-01-06 21:34:56 +08:00
andy.lee da77762b7f add mergify.yml
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 21:32:00 +08:00
71 changed files with 2022 additions and 3220 deletions
-1
View File
@@ -2,7 +2,6 @@ pull_request_rules:
- name: Automatically open backport PR to release-harvester-v1.5 - name: Automatically open backport PR to release-harvester-v1.5
conditions: conditions:
- base=main - base=main
- label="require backport 1.5"
actions: actions:
backport: backport:
branches: branches:
@@ -22,7 +22,7 @@ jobs:
- name: Determine release tag - name: Determine release tag
id: determine_tag id: determine_tag
run: | run: |
if [[ "${{ github.event.release.tag_name }}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then if [[ "${{ github.ref_name }}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
RELEASE_TAG="harvester-${BASH_REMATCH[1]}" RELEASE_TAG="harvester-${BASH_REMATCH[1]}"
echo "${RELEASE_TAG}" echo "${RELEASE_TAG}"
echo "release_tag=${RELEASE_TAG}" >> $GITHUB_OUTPUT echo "release_tag=${RELEASE_TAG}" >> $GITHUB_OUTPUT
+15 -2
View File
@@ -8,7 +8,20 @@ From Rancher 2.10.0, Harvester integration is provided by this new Harvester UI
## Installation ## Installation
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. 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
## Running for Development ## Running for Development
@@ -57,7 +70,7 @@ For developers, after reading through the introduction on this page, head over t
License License
======= =======
Copyright (c) 2014-2025 [SUSE, LLC.](https://www.suse.com/) Copyright (c) 2014-2023 [Rancher Labs, Inc.](http://rancher.com)
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "harvester-ui-extension", "name": "harvester-ui-extension",
"version": "1.5.1", "version": "1.0.6",
"private": false, "private": false,
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
}, },
"dependencies": { "dependencies": {
"@rancher/shell": "3.0.2-rc.6", "@rancher/shell": "3.0.1-rc.4",
"cache-loader": "^4.1.0", "cache-loader": "^4.1.0",
"color": "4.2.3", "color": "4.2.3",
"ip": "2.0.1", "ip": "2.0.1",
+8 -35
View File
@@ -5,10 +5,8 @@ import { Checkbox } from '@components/Form/Checkbox';
import ModalWithCard from '@shell/components/ModalWithCard'; import ModalWithCard from '@shell/components/ModalWithCard';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { HCI } from '../types'; import { HCI } from '../types';
import UpgradeInfo from './UpgradeInfo'; import UpgradeInfo from './UpgradeInfo';
export default { export default {
name: 'HarvesterUpgrade', name: 'HarvesterUpgrade',
@@ -30,15 +28,14 @@ export default {
data() { data() {
return { return {
upgrade: [], upgrade: [],
upgradeMessage: [], upgradeMessage: [],
errors: '', errors: '',
selectMode: true, selectMode: true,
version: '', version: '',
enableLogging: true, enableLogging: true,
skipSingleReplicaDetachedVol: false, readyReleaseNote: false,
readyReleaseNote: false, isOpen: false
isOpen: false
}; };
}, },
@@ -71,10 +68,6 @@ export default {
return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG); return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG);
}, },
skipSingleReplicaDetachedVolFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
},
releaseLink() { releaseLink() {
return `https://github.com/harvester/harvester/releases/tag/${ this.version }`; return `https://github.com/harvester/harvester/releases/tag/${ this.version }`;
} }
@@ -111,11 +104,6 @@ export default {
spec: { version: this.version } 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) { if (this.canEnableLogging) {
upgradeValue.spec.logEnabled = this.enableLogging; upgradeValue.spec.logEnabled = this.enableLogging;
} }
@@ -202,21 +190,6 @@ export default {
/> />
</div> </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"> <div v-if="version">
<p <p
v-clean-html="t('harvester.upgradePage.releaseTip', {url: releaseLink}, true)" v-clean-html="t('harvester.upgradePage.releaseTip', {url: releaseLink}, true)"
+1 -1
View File
@@ -43,7 +43,7 @@ export default {
{{ t('harvester.upgradePage.upgradeInfo.tip') }} {{ t('harvester.upgradePage.upgradeInfo.tip') }}
</p> </p>
<p> <p class="mb-5">
{{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a {{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a
:href="releaseVersion" :href="releaseVersion"
target="_blank" target="_blank"
@@ -174,6 +174,10 @@ export default {
return driver === LONGHORN_DRIVER || this.allowEmptySnapshotClassNameFeatureEnabled; return driver === LONGHORN_DRIVER || this.allowEmptySnapshotClassNameFeatureEnabled;
}, },
isBackupVolumeSnapshotClassNameSelectable(driver) {
return driver === LONGHORN_DRIVER;
},
add() { add() {
this.configArr.push({ this.configArr.push({
key: '', key: '',
@@ -243,6 +247,7 @@ export default {
:options="getVolumeSnapshotOptions(driver.key)" :options="getVolumeSnapshotOptions(driver.key)"
:required="isBackupVolumeSnapshotRequired(driver.key)" :required="isBackupVolumeSnapshotRequired(driver.key)"
:label="t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName')" :label="t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName')"
@selectable="isBackupVolumeSnapshotClassNameSelectable(driver.key)"
@keydown.native.enter.prevent="()=>{}" @keydown.native.enter.prevent="()=>{}"
@update:value="update" @update:value="update"
/> />
@@ -1,5 +1,6 @@
<script> <script>
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
@@ -61,7 +62,7 @@ export default {
<div <div
class="row" class="row"
@input="update" @update:value="update"
> >
<div class="col span-12"> <div class="col span-12">
<LabeledInput <LabeledInput
@@ -12,9 +12,6 @@ import { NODE } from '@shell/config/types';
import { HCI } from '../../types'; import { HCI } from '../../types';
import { DOC } from '../../config/doc-links'; import { DOC } from '../../config/doc-links';
import { docLink } from '../../utils/feature-flags'; import { docLink } from '../../utils/feature-flags';
import { NETWORK_TYPE } from '../../config/types';
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
export default { export default {
name: 'HarvesterEditStorageNetwork', name: 'HarvesterEditStorageNetwork',
@@ -60,14 +57,11 @@ export default {
data() { data() {
let parsedDefaultValue = {}; let parsedDefaultValue = {};
let openVlan = false; let openVlan = false;
let networkType = L2VLAN;
try { try {
parsedDefaultValue = JSON.parse(this.value.value); 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; openVlan = true;
} catch (error) { } catch (error) {
networkType = L2VLAN;
parsedDefaultValue = { parsedDefaultValue = {
vlan: '', vlan: '',
clusterNetwork: '', clusterNetwork: '',
@@ -79,7 +73,6 @@ export default {
return { return {
openVlan, openVlan,
networkType,
errors: [], errors: [],
exclude, exclude,
parsedDefaultValue, parsedDefaultValue,
@@ -94,35 +87,16 @@ export default {
}, },
computed: { computed: {
showVlan() {
return this.networkType === L2VLAN;
},
networkTypes() {
const types = [L2VLAN];
if (this.untaggedNetworkSettingEnabled) {
types.push(UNTAGGED);
}
return types;
},
storageNetworkExampleLink() { storageNetworkExampleLink() {
const version = this.$store.getters['harvester-common/getServerVersion'](); const version = this.$store.getters['harvester-common/getServerVersion']();
return docLink(DOC.STORAGE_NETWORK_EXAMPLE, version); return docLink(DOC.STORAGE_NETWORK_EXAMPLE, version);
}, },
untaggedNetworkSettingEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('untaggedNetworkSetting');
},
clusterNetworkOptions() { clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || []; 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 clusterNetworksOptions.map((n) => { return clusterNetworks.map((n) => {
const disabled = !n.isReadyForStorageNetwork; const disabled = !n.isReadyForStorageNetwork;
return { return {
@@ -134,50 +108,7 @@ export default {
}, },
}, },
watch: {
networkType: {
handler(neu) {
this.parsedDefaultValue.clusterNetwork = '';
if (neu === L2VLAN) {
this.parsedDefaultValue.vlan = '';
} else {
delete this.parsedDefaultValue.vlan;
}
},
deep: true
}
},
methods: { 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() { update() {
const exclude = this.exclude.filter((ip) => ip); const exclude = this.exclude.filter((ip) => ip);
@@ -207,7 +138,7 @@ export default {
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true)); errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true));
} }
if (this.networkType === L2VLAN && !this.parsedDefaultValue.vlan) { if (!this.parsedDefaultValue.vlan) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true)); errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
} }
@@ -260,25 +191,13 @@ export default {
/> />
<div v-if="openVlan"> <div v-if="openVlan">
<LabeledSelect
v-model:value="networkType"
class="mb-20"
:options="networkTypes"
:mode="mode"
:label="t('harvester.fields.type')"
required
/>
<LabeledInput <LabeledInput
v-if="showVlan"
v-model:value.number="parsedDefaultValue.vlan" v-model:value.number="parsedDefaultValue.vlan"
type="number" type="number"
class="mb-20" class="mb-20"
:mode="mode" :mode="mode"
required required
placeholder="e.g. 1 - 4094"
label-key="harvester.setting.storageNetwork.vlan" label-key="harvester.setting.storageNetwork.vlan"
@update:value="inputVlan"
/> />
<LabeledSelect <LabeledSelect
+68 -53
View File
@@ -1,55 +1,70 @@
import semver from 'semver'; // https://github.com/harvester/dashboard/releases/tag/v1.3.0
const featuresV130 = [
'supportHarvesterClusterVersion'
];
const FEATURE_FLAGS = { // https://github.com/harvester/dashboard/releases/tag/v1.3.1
'v1.3.0': [ const featuresV131 = [
'supportHarvesterClusterVersion' ...featuresV130,
], 'autoRotateRke2CertsSetting',
'v1.3.1': [ 'supportBundleNodeCollectionTimeoutSetting'
'autoRotateRke2CertsSetting', ];
'supportBundleNodeCollectionTimeoutSetting'
], // https://github.com/harvester/dashboard/releases/tag/v1.3.2
'v1.3.2': [ const featuresV132 = [
'kubeconfigDefaultTokenTTLMinutesSetting', ...featuresV131,
'improveMaintenanceMode', 'kubeconfigDefaultTokenTTLMinutesSetting',
], 'improveMaintenanceMode',
'v1.3.3': [], ];
'v1.4.0': [
'cpuPinning', // TODO: add v1.3.3 official release note
'usbPassthrough', // https://github.com/harvester/dashboard/releases/tag/v1.3.3-dev-20250105
'volumeEncryption', const featuresV133 = [
'schedulingVMBackup', ...featuresV132,
'vmSnapshotQuota', ];
'longhornV2LVMSupport',
'improveMaintenanceMode', // https://github.com/harvester/dashboard/releases/tag/v1.4.0
], const featuresV140 = [
'v1.4.1': [], ...featuresV132,
'v1.4.2': [ 'cpuPinning',
'refreshIntervalInSecond', 'usbPassthrough',
'allowEmptySnapshotClassName' 'volumeEncryption',
], 'schedulingVMBackup',
'v1.4.3': [], 'vmSnapshotQuota',
'v1.5.0': [ 'longhornV2LVMSupport',
'tpmPersistentState', 'improveMaintenanceMode',
'efiPersistentState', ];
'untaggedNetworkSetting',
'skipSingleReplicaDetachedVol', // https://github.com/harvester/dashboard/releases/tag/v1.4.1
'thirdPartyStorage' const featuresV141 = [
], ...featuresV140
'v1.5.1': [], ];
// 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 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();
+1 -3
View File
@@ -31,8 +31,7 @@ import {
FINGERPRINT, FINGERPRINT,
IMAGE_PROGRESS, IMAGE_PROGRESS,
SNAPSHOT_TARGET_VOLUME, SNAPSHOT_TARGET_VOLUME,
IMAGE_VIRTUAL_SIZE, IMAGE_VIRTUAL_SIZE
IMAGE_STORAGE_CLASS
} from './table-headers'; } from './table-headers';
const TEMPLATE = HCI.VM_VERSION; const TEMPLATE = HCI.VM_VERSION;
@@ -222,7 +221,6 @@ export function init($plugin, store) {
STATE, STATE,
NAME_COL, NAME_COL,
NAMESPACE_COL, NAMESPACE_COL,
IMAGE_STORAGE_CLASS,
IMAGE_PROGRESS, IMAGE_PROGRESS,
IMAGE_DOWNLOAD_SIZE, IMAGE_DOWNLOAD_SIZE,
IMAGE_VIRTUAL_SIZE, IMAGE_VIRTUAL_SIZE,
-1
View File
@@ -1,4 +1,3 @@
// TODO: delete this not used variable
export const MemoryUnit = [{ export const MemoryUnit = [{
label: 'Mi', label: 'Mi',
value: 'Mi' value: 'Mi'
+56 -60
View File
@@ -7,64 +7,60 @@ export const LABELS_TO_IGNORE_REGEX = [
]; ];
export const HCI = { export const HCI = {
CLOUD_INIT: 'harvesterhci.io/cloud-init-template', CLOUD_INIT: 'harvesterhci.io/cloud-init-template',
CURRENT_IP: 'rke2.io/internal-ip', CURRENT_IP: 'rke2.io/internal-ip',
IMAGE_ID: 'harvesterhci.io/imageId', IMAGE_ID: 'harvesterhci.io/imageId',
SSH_NAMES: 'harvesterhci.io/sshNames', SSH_NAMES: 'harvesterhci.io/sshNames',
NETWORK_IPS: 'network.harvesterhci.io/ips', NETWORK_IPS: 'network.harvesterhci.io/ips',
TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName', TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName',
CREATOR: 'harvesterhci.io/creator', CREATOR: 'harvesterhci.io/creator',
OS: 'harvesterhci.io/os', OS: 'harvesterhci.io/os',
GOLDEN_IMAGE: 'harvesterhci.io/goldenImage', NETWORK_TYPE: 'network.harvesterhci.io/type',
NETWORK_TYPE: 'network.harvesterhci.io/type', VM_NAME: 'harvesterhci.io/vmName',
VM_NAME: 'harvesterhci.io/vmName', VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix', VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory', MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status', HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name', HOST_CONSOLE_URL: 'harvesterhci.io/host-console-url',
HOST_CONSOLE_URL: 'harvesterhci.io/host-console-url', RESTORE_NAME: 'restore.harvesterhci.io/name',
RESTORE_NAME: 'restore.harvesterhci.io/name', NODE_ROLE_MASTER: 'node-role.kubernetes.io/master',
NODE_ROLE_MASTER: 'node-role.kubernetes.io/master', NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane', NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness', PROMOTE_STATUS: 'harvesterhci.io/promote-status',
PROMOTE_STATUS: 'harvesterhci.io/promote-status', MIGRATION_STATE: 'harvesterhci.io/migrationState',
MIGRATION_STATE: 'harvesterhci.io/migrationState', VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates', IMAGE_NAME: 'harvesterhci.io/image-name',
IMAGE_NAME: 'harvesterhci.io/image-name', INIT_IP: 'etcd.rke2.cattle.io/node-address',
INIT_IP: 'etcd.rke2.cattle.io/node-address', NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
NODE_SCHEDULABLE: 'kubevirt.io/schedulable', NETWORK_ROUTE: 'network.harvesterhci.io/route',
NETWORK_ROUTE: 'network.harvesterhci.io/route', MATCHED_NODES: 'network.harvesterhci.io/matched-nodes',
MATCHED_NODES: 'network.harvesterhci.io/matched-nodes', OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image', LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade', UPGRADE_STATE: 'harvesterhci.io/upgradeState',
UPGRADE_STATE: 'harvesterhci.io/upgradeState', REAY_MESSAGE: 'harvesterhci.io/read-message',
REAY_MESSAGE: 'harvesterhci.io/read-message', DYNAMIC_SSHKEYS_NAMES: 'harvesterhci.io/dynamic-ssh-key-names',
DYNAMIC_SSHKEYS_NAMES: 'harvesterhci.io/dynamic-ssh-key-names', DYNAMIC_SSHKEYS_USERS: 'harvesterhci.io/dynamic-ssh-key-users',
DYNAMIC_SSHKEYS_USERS: 'harvesterhci.io/dynamic-ssh-key-users', IMAGE_SUFFIX: 'harvesterhci.io/image-type',
IMAGE_SUFFIX: 'harvesterhci.io/image-type', OS_TYPE: 'harvesterhci.io/os-type',
OS_TYPE: 'harvesterhci.io/os-type', STORAGE_PROVISIONER: 'harvesterhci.io/storageProvisioner',
STORAGE_PROVISIONER: 'harvesterhci.io/storageProvisioner', HOST_REQUEST: 'management.cattle.io/pod-requests',
SKIP_SINGLE_REPLICA_DETACHED_VOL: 'harvesterhci.io/skipSingleReplicaDetachedVol', STORAGE_CLASS: 'harvesterhci.io/storageClassName',
HOST_REQUEST: 'management.cattle.io/pod-requests', STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
STORAGE_CLASS: 'harvesterhci.io/storageClassName', ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io', VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental', KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error', KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx',
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine', NODE_MANUFACTURER: 'manufacturer',
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm', NODE_MODEL: 'model',
KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx', NODE_SERIAL_NUMBER: 'serialNumber',
NODE_MANUFACTURER: 'manufacturer', VM_INSUFFICIENT: 'harvesterhci.io/insufficient-resource-quota',
NODE_MODEL: 'model', NODE_NTP_SYNC_STATUS: 'node.harvesterhci.io/ntp-service',
NODE_SERIAL_NUMBER: 'serialNumber', PARENT_SRIOV: 'harvesterhci.io/parent-sriov-network-device',
VM_INSUFFICIENT: 'harvesterhci.io/insufficient-resource-quota', PARENT_SRIOV_GPU: 'harvesterhci.io/parentSRIOVGPUDevice',
NODE_NTP_SYNC_STATUS: 'node.harvesterhci.io/ntp-service', VM_MAINTENANCE_MODE_STRATEGY: 'harvesterhci.io/maintain-mode-strategy',
PARENT_SRIOV: 'harvesterhci.io/parent-sriov-network-device', NODE_CPU_MANAGER_UPDATE_STATUS: 'harvesterhci.io/cpu-manager-update-status',
PARENT_SRIOV_GPU: 'harvesterhci.io/parentSRIOVGPUDevice', CPU_MANAGER: 'cpumanager',
VM_MAINTENANCE_MODE_STRATEGY: 'harvesterhci.io/maintain-mode-strategy', VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails',
NODE_CPU_MANAGER_UPDATE_STATUS: 'harvesterhci.io/cpu-manager-update-status', SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId',
CPU_MANAGER: 'cpumanager', DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine',
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'
}; };
+1 -3
View File
@@ -65,9 +65,7 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: {}, [HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: {},
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { featureFlag: 'supportBundleNodeCollectionTimeoutSetting' }, [HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' }, [HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' },
[HCI_SETTING.STORAGE_NETWORK]: { [HCI_SETTING.STORAGE_NETWORK]: { kind: 'custom', from: 'import' },
kind: 'custom', from: 'import', canReset: true
},
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' }, [HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' }, [HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
[HCI_SETTING.SSL_PARAMETERS]: { [HCI_SETTING.SSL_PARAMETERS]: {
-10
View File
@@ -88,13 +88,3 @@ export const MACHINE_POOLS = {
align: 'center', align: 'center',
width: 100, 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,
};
-10
View File
@@ -2,13 +2,3 @@ export const BACKUP_TYPE = {
BACKUP: 'backup', BACKUP: 'backup',
SNAPSHOT: 'snapshot' SNAPSHOT: 'snapshot'
}; };
export const NETWORK_TYPE = {
L2VLAN: 'L2VlanNetwork',
UNTAGGED: 'UntaggedNetwork'
};
export const VOLUME_MODE = {
BLOCK: 'Block',
FILE_SYSTEM: 'Filesystem'
};
@@ -4,7 +4,6 @@ import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { LONGHORN, METRIC } from '@shell/config/types'; import { LONGHORN, METRIC } from '@shell/config/types';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import { UNIT_SUFFIX } from '../../utils/unit';
import HarvesterCPUUsed from '../../formatters/HarvesterCPUUsed'; import HarvesterCPUUsed from '../../formatters/HarvesterCPUUsed';
import HarvesterMemoryUsed from '../../formatters/HarvesterMemoryUsed'; import HarvesterMemoryUsed from '../../formatters/HarvesterMemoryUsed';
import HarvesterStorageUsed from '../../formatters/HarvesterStorageUsed'; import HarvesterStorageUsed from '../../formatters/HarvesterStorageUsed';
@@ -122,7 +121,7 @@ export default {
memoryUnits() { memoryUnits() {
const exponent = exponentNeeded(this.memoryTotal, 1024); const exponent = exponentNeeded(this.memoryTotal, 1024);
return `${ UNITS[exponent] }${ UNIT_SUFFIX }`; return `${ UNITS[exponent] }iB`;
}, },
nodeType() { nodeType() {
@@ -9,7 +9,7 @@ import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
import Loading from '@shell/components/Loading.vue'; import Loading from '@shell/components/Loading.vue';
import SortableTable from '@shell/components/SortableTable'; import SortableTable from '@shell/components/SortableTable';
import Banner from '@components/Banner/Banner.vue'; import Banner from '@components/Banner/Banner.vue';
import { UNIT_SUFFIX } from '../../utils/unit';
import metricPoller from '@shell/mixins/metric-poller'; import metricPoller from '@shell/mixins/metric-poller';
import { import {
METRIC, NODE, LONGHORN, POD, EVENT METRIC, NODE, LONGHORN, POD, EVENT
@@ -178,7 +178,7 @@ export default {
minExponent: 3, minExponent: 3,
maxExponent: 3, maxExponent: 3,
maxPrecision: 2, maxPrecision: 2,
suffix: UNIT_SUFFIX, suffix: 'iB',
}; };
const longhornDisks = Object.keys(diskStatus).map((key) => { const longhornDisks = Object.keys(diskStatus).map((key) => {
+5 -6
View File
@@ -106,12 +106,6 @@ export default {
:label="t('harvester.modal.cloneVM.name')" :label="t('harvester.modal.cloneVM.name')"
required required
/> />
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template> </template>
<template <template
@@ -133,6 +127,11 @@ export default {
@click="create" @click="create"
/> />
</div> </div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</template> </template>
</Card> </Card>
</template> </template>
@@ -1,175 +0,0 @@
<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,25 +42,8 @@ export default {
computed: { computed: {
...mapState('action-menu', ['modalData']), ...mapState('action-menu', ['modalData']),
title() { warningMessageKey() {
return this.modalData.title || 'dialog.promptRemove.title'; return this.modalData.warningMessageKey;
},
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() { names() {
@@ -87,12 +70,6 @@ export default {
return this.resources[0].nameDisplay; return this.resources[0].nameDisplay;
}, },
needConfirmation() {
const { needConfirmation = true } = this.modalData ;
return needConfirmation === true;
},
plusMore() { plusMore() {
const remaining = this.resources.length - this.names.length; const remaining = this.resources.length - this.names.length;
@@ -120,15 +97,11 @@ export default {
}, },
deleteDisabled() { deleteDisabled() {
if (!this.needConfirmation) {
return false;
}
return this.confirmName !== this.nameToMatch; return this.confirmName !== this.nameToMatch;
}, },
protip() { protip() {
return this.t('dialog.promptRemove.protip', { alternateLabel }); return this.t('promptRemove.protip', { alternateLabel });
}, },
}, },
@@ -164,39 +137,31 @@ export default {
> >
<template #title> <template #title>
<h4 class="text-default-text"> <h4 class="text-default-text">
{{ t(title, { type }, true) }} {{ t('promptRemove.title') }}
</h4> </h4>
</template> </template>
<template #body> <template #body>
<div class="pl-10 pr-10"> <div class="pl-10 pr-10">
<span <span
v-clean-html="warningMessage" v-clean-html="t(warningMessageKey, { type, names: resourceNames }, true)"
></span> ></span>
<div <div class="mt-10 mb-10">
v-if="needConfirmation" <span
class="mt-20" v-clean-html="t('promptRemove.confirmName', { nameToMatch: escapeHtml(nameToMatch) }, true)"
> ></span>
<div class="mt-10 mb-10"> </div>
<span <div class="mb-10">
v-clean-html="t('dialog.promptRemove.confirmName', { <CopyToClipboardText :text="nameToMatch" />
type: formattedType, </div>
nameToMatch: escapeHtml(nameToMatch) <input
}, true)" id="confirm"
></span> v-model="confirmName"
</div> type="text"
<div class="mb-10"> />
<CopyToClipboardText :text="nameToMatch" /> <div class="text-info mt-20">
</div> {{ protip }}
<input
id="confirm"
v-model="confirmName"
type="text"
/>
<div class="text-info mt-20">
{{ protip }}
</div>
</div> </div>
<Banner <Banner
v-for="(error, i) in errors" v-for="(error, i) in errors"
@@ -57,10 +57,6 @@ export default {
if (!!pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) { if (!!pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
return false; return false;
} }
// we won't show golden image volume in the hot plug volume modal
if (pvc.isGoldenImageVolume) {
return false;
}
return !pvc.attachVM; return !pvc.attachVM;
}) })
@@ -34,14 +34,9 @@ export default {
await allHash(hash); await allHash(hash);
const allStorages = this.allStorageClasses; const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find((s) => s.isDefault);
const defaultStorage = allStorages.find((s) => s.isDefault);
if (this.isLonghornV1Volume) { this['storageClassName'] = defaultStorage?.metadata?.name || 'longhorn';
this['storageClassName'] = defaultStorage?.metadata?.name || 'longhorn';
} else {
this['storageClassName'] = this.nonLonghornV1StorageClasses[0]?.metadata?.name || '';
}
}, },
data() { data() {
@@ -59,7 +54,7 @@ export default {
...mapGetters({ t: 'i18n/t' }), ...mapGetters({ t: 'i18n/t' }),
actionResource() { actionResource() {
return this.resources[0] || {}; return this.resources[0];
}, },
namespaces() { namespaces() {
@@ -78,33 +73,15 @@ export default {
return out; return out;
}, },
isLonghornV1Volume() {
return this.actionResource?.isLonghornV1 === true;
},
disableSave() { disableSave() {
return !(this.name && this.namespace && this.storageClassName); return !(this.name && this.namespace && this.storageClassName);
}, },
allStorageClasses() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/all`](STORAGE_CLASS) || [];
},
nonLonghornV1StorageClasses() {
return this.allStorageClasses.filter((s) => s.isLonghornV1 === false) || [];
},
storageClassOptions() { storageClassOptions() {
let storages = this.allStorageClasses; const inStore = this.$store.getters['currentProduct'].inStore;
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
// Volume with non-longhorn v1 sc can't be exported to longhorn v1 sc image const out = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
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; const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
return { return {
@@ -113,7 +90,7 @@ export default {
}; };
}) || []; }) || [];
return options; return out;
}, },
}, },
@@ -1,165 +0,0 @@
<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,7 +5,6 @@ import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton'; import AsyncButton from '@shell/components/AsyncButton';
import UnitInput from '@shell/components/form/UnitInput'; import UnitInput from '@shell/components/form/UnitInput';
import { exceptionToErrorsArray } from '@shell/utils/error'; import { exceptionToErrorsArray } from '@shell/utils/error';
import { GIBIBYTE } from '../utils/unit';
export default { export default {
name: 'HarvesterVMQuotaDialog', name: 'HarvesterVMQuotaDialog',
@@ -30,7 +29,6 @@ export default {
data() { data() {
return { return {
GIBIBYTE,
totalSnapshotSize: '', totalSnapshotSize: '',
errors: [] errors: []
}; };
@@ -46,6 +44,7 @@ export default {
}, },
methods: { methods: {
close() { close() {
this.totalSnapshotSize = ''; this.totalSnapshotSize = '';
this.$emit('close'); this.$emit('close');
@@ -93,7 +92,7 @@ export default {
:input-exponent="3" :input-exponent="3"
:increment="1024" :increment="1024"
:output-modifier="true" :output-modifier="true"
:suffix="GIBIBYTE" suffix="GiB"
class="mb-20" class="mb-20"
/> />
<Banner <Banner
@@ -20,7 +20,7 @@ import Loading from '@shell/components/Loading.vue';
import MessageLink from '@shell/components/MessageLink'; import MessageLink from '@shell/components/MessageLink';
import { ADD_ONS } from '@pkg/harvester/config/harvester-map'; import { ADD_ONS } from '@pkg/harvester/config/harvester-map';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester'; 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 { _EDIT } from '@shell/config/query-params';
import { sortBy } from '@shell/utils/sort'; import { sortBy } from '@shell/utils/sort';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
@@ -185,7 +185,7 @@ export default {
minExponent: 3, minExponent: 3,
maxExponent: 3, maxExponent: 3,
maxPrecision: 2, maxPrecision: 2,
suffix: UNIT_SUFFIX, suffix: 'iB',
}; };
const longhornDisks = Object.keys(diskStatus).map((key) => { const longhornDisks = Object.keys(diskStatus).map((key) => {
@@ -457,7 +457,7 @@ export default {
const devPath = d.spec?.devPath; const devPath = d.spec?.devPath;
const deviceType = d.status?.deviceStatus?.details?.deviceType; const deviceType = d.status?.deviceStatus?.details?.deviceType;
const sizeBytes = d.status?.deviceStatus?.capacity?.sizeBytes; const sizeBytes = d.status?.deviceStatus?.capacity?.sizeBytes;
const size = formatSi(sizeBytes, { increment: 1024, suffix: UNIT_SUFFIX }); const size = formatSi(sizeBytes, { increment: 1024 });
const parentDevice = d.status?.deviceStatus?.parentDevice; const parentDevice = d.status?.deviceStatus?.parentDevice;
const isChildAdded = this.newDisks.find((newDisk) => newDisk.blockDevice?.status?.deviceStatus?.parentDevice === devPath); const isChildAdded = this.newDisks.find((newDisk) => newDisk.blockDevice?.status?.deviceStatus?.parentDevice === devPath);
const name = d.displayName; const name = d.displayName;
@@ -11,9 +11,6 @@ import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-anno
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise'; import { allHash } from '@shell/utils/promise';
import { HCI } from '../types'; import { HCI } from '../types';
import { NETWORK_TYPE } from '../config/types';
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
const AUTO = 'auto'; const AUTO = 'auto';
const MANUAL = 'manual'; const MANUAL = 'manual';
@@ -51,7 +48,7 @@ export default {
config.bridge = config.bridge.slice(0, -3); config.bridge = config.bridge.slice(0, -3);
} }
const type = this.value.vlanType || L2VLAN ; const type = this.value.vlanType || 'L2VlanNetwork' ;
return { return {
config, config,
@@ -104,15 +101,15 @@ export default {
}, },
networkType() { networkType() {
return [L2VLAN, UNTAGGED]; return ['L2VlanNetwork', 'UntaggedNetwork'];
}, },
isUntaggedNetwork() { isUntaggedNetwork() {
if (this.isView) { if (this.isView) {
return this.value.vlanType === UNTAGGED; return this.value.vlanType === 'UntaggedNetwork';
} }
return this.type === UNTAGGED; return this.type === 'UntaggedNetwork';
} }
}, },
@@ -23,9 +23,10 @@ import Tags from '../../components/DiskTags';
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim'; import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass'; 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'; 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_IMMEDIATE = 'Immediate';
const VOLUME_BINDING_MODE_WAIT = 'WaitForFirstConsumer'; const VOLUME_BINDING_MODE_WAIT = 'WaitForFirstConsumer';
@@ -63,7 +63,7 @@ export default {
diskSelector: null, diskSelector: null,
nodeSelector: null, nodeSelector: null,
encrypted: 'false', encrypted: 'false',
migratable: this.value.thirdPartyStorageFeatureEnabled ? 'true' : 'false', migratable: 'false',
dataEngine: DATA_ENGINE_V2 dataEngine: DATA_ENGINE_V2
}; };
} }
@@ -318,7 +318,7 @@ export default {
:label="t('harvester.storage.parameters.migratable.label')" :label="t('harvester.storage.parameters.migratable.label')"
:mode="mode" :mode="mode"
:options="migratableOptions" :options="migratableOptions"
:disabled="!value.thirdPartyStorageFeatureEnabled" :disabled="true"
/> />
</div> </div>
<div class="row mt-20"> <div class="row mt-20">
@@ -24,7 +24,6 @@ const CLONE = 'clone';
const DOWNLOAD = 'download'; const DOWNLOAD = 'download';
const UPLOAD = 'upload'; const UPLOAD = 'upload';
const rawORqcow2 = 'raw_qcow2'; const rawORqcow2 = 'raw_qcow2';
const LONGHORN = 'longhorn';
export default { export default {
name: 'EditImage', name: 'EditImage',
@@ -62,10 +61,10 @@ export default {
storageClasses: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }), storageClasses: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
}); });
this['storageClassName'] = this.storageClassName || this.defaultStorageClassName(); const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find((s) => s.isDefault);
this.images = this.$store.getters[`${ inStore }/all`](HCI.IMAGE);
this.storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS); this['storageClassName'] = this.storageClassName || defaultStorage?.metadata?.name || 'longhorn';
this.images = this.$store.getters[`${ inStore }/all`](HCI.IMAGE);
const { securityParameters } = this.value.spec; const { securityParameters } = this.value.spec;
@@ -101,15 +100,14 @@ export default {
} }
return { return {
selectedImage: null, selectedImage: null,
storageClasses: [], images: [],
images: [], url: this.value.spec.url,
url: this.value.spec.url, files: [],
files: [], resource: '',
resource: '', headers: {},
headers: {}, fileUrl: '',
fileUrl: '', file: '',
file: '',
}; };
}, },
@@ -146,26 +144,12 @@ export default {
]; ];
}, },
encryptedStorageClasses() {
const inStore = this.$store.getters['currentProduct'].inStore;
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
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() { storageClassOptions() {
const storages = this.value.spec?.securityParameters?.cryptoOperation === ENCRYPT ? this.encryptedStorageClasses : this.nonEncryptedStorageClasses; const inStore = this.$store.getters['currentProduct'].inStore;
const filteredStorages = this.value.thirdPartyStorageFeatureEnabled ? storages.filter((s) => !s.parameters?.backingImage) : storages const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
.filter((s) => !s.parameters?.backingImage && s.provisioner !== LVM_DRIVER) ;
return filteredStorages return storages
.filter((s) => !s.parameters?.backingImage && s.provisioner !== LVM_DRIVER) // Lvm storage is not supported.
.map((s) => { .map((s) => {
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name; const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
@@ -183,9 +167,6 @@ export default {
set(nue) { set(nue) {
this.value.metadata.annotations[HCI_ANNOTATIONS.STORAGE_CLASS] = nue; this.value.metadata.annotations[HCI_ANNOTATIONS.STORAGE_CLASS] = nue;
if (this.value.thirdPartyStorageFeatureEnabled) {
this.value.spec.targetStorageClassName = nue;
}
} }
}, },
sourceImageOptions() { sourceImageOptions() {
@@ -258,20 +239,6 @@ export default {
this.$refs.file.value = null; 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: { methods: {
@@ -418,18 +385,6 @@ export default {
return str.toLowerCase().includes(os.value.toLowerCase()) ? os.value : false; 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,11 +6,12 @@ import { Checkbox } from '@components/Form/Checkbox';
import CruResource from '@shell/components/CruResource'; import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription'; import NameNsDescription from '@shell/components/form/NameNsDescription';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import { Banner } from '@components/Banner'; import Labels from '@shell/components/form/Labels';
import KeyValue from '@shell/components/form/KeyValue'; import KeyValue from '@shell/components/form/KeyValue';
import NodeScheduling from '@shell/components/form/NodeScheduling'; import NodeScheduling from '@shell/components/form/NodeScheduling';
import PodAffinity from '@shell/components/form/PodAffinity'; import PodAffinity from '@shell/components/form/PodAffinity';
import UnitInput from '@shell/components/form/UnitInput'; import UnitInput from '@shell/components/form/UnitInput';
import { randomStr } from '@shell/utils/string'; import { randomStr } from '@shell/utils/string';
import { _CONFIG, _EDIT, _VIEW } from '@shell/config/query-params'; import { _CONFIG, _EDIT, _VIEW } from '@shell/config/query-params';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
@@ -46,7 +47,7 @@ export default {
PodAffinity, PodAffinity,
Reserved, Reserved,
UnitInput, UnitInput,
Banner, Labels,
KeyValue, KeyValue,
}, },
@@ -320,44 +321,34 @@ export default {
</Tab> </Tab>
<Tab <Tab
name="labels" :name="t('generic.labels')"
:label="t('generic.labels')" :label="t('harvester.tab.instanceLabel')"
:weight="-9" :weight="-5"
> >
<Banner color="info"> <Labels
<t k="harvester.virtualMachine.labels.banner" /> :default-container-class="'labels-and-annotations-container'"
</Banner> :value="value"
<KeyValue
key="labels"
:value="value.labels"
:add-label="t('labels.addLabel')"
:mode="mode" :mode="mode"
:read-allowed="false" :display-side-by-side="false"
:value-can-be-empty="true" :show-annotations="false"
@update:value="value.setLabels($event)" :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>
</Tab> </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 <Tab
name="advanced" name="advanced"
:label="t('harvester.tab.advanced')" :label="t('harvester.tab.advanced')"
@@ -443,7 +434,7 @@ export default {
<div class="spacer"></div> <div class="spacer"></div>
<Checkbox <Checkbox
v-if="value.cpuPinningFeatureEnabled" v-if="value.cpuPinningFeatureEnabled"
v-model:value="cpuPinning" v-model="cpuPinning"
class="check" class="check"
type="checkbox" type="checkbox"
tooltip-key="harvester.virtualMachine.cpuPinning.tooltip" tooltip-key="harvester.virtualMachine.cpuPinning.tooltip"
@@ -474,29 +465,11 @@ export default {
:mode="mode" :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 <Checkbox
v-model:value="efiEnabled" v-model:value="efiEnabled"
class="check" class="check"
type="checkbox" type="checkbox"
:label="t('harvester.virtualMachine.advancedOptions.efiEnabled')" :label="t('harvester.virtualMachine.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" :mode="mode"
/> />
@@ -505,7 +478,7 @@ export default {
v-model:value="secureBoot" v-model:value="secureBoot"
class="check" class="check"
type="checkbox" type="checkbox"
:label="t('harvester.virtualMachine.advancedOptions.secureBoot')" :label="t('harvester.virtualMachine.secureBoot')"
:mode="mode" :mode="mode"
/> />
</Tab> </Tab>
+5 -81
View File
@@ -21,8 +21,6 @@ import { InterfaceOption, VOLUME_DATA_SOURCE_KIND } from '../config/harvester-ma
import { HCI, VOLUME_SNAPSHOT } from '../types'; import { HCI, VOLUME_SNAPSHOT } from '../types';
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass'; import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
import { DATA_ENGINE_V2 } from '../models/harvester/persistentvolumeclaim'; import { DATA_ENGINE_V2 } from '../models/harvester/persistentvolumeclaim';
import { GIBIBYTE } from '../utils/unit';
import { VOLUME_MODE } from '@pkg/harvester/config/types';
export default { export default {
name: 'HarvesterVolume', name: 'HarvesterVolume',
@@ -74,8 +72,7 @@ export default {
data() { data() {
if (this.mode === _CREATE) { if (this.mode === _CREATE) {
// default volumeMode to Block this.value.spec.volumeMode = 'Block';
this.value.spec.volumeMode = VOLUME_MODE.BLOCK;
this.value.spec.accessModes = ['ReadWriteMany']; this.value.spec.accessModes = ['ReadWriteMany'];
} }
@@ -89,7 +86,6 @@ export default {
imageId, imageId,
snapshots: [], snapshots: [],
images: [], images: [],
GIBIBYTE
}; };
}, },
@@ -128,10 +124,6 @@ export default {
return InterfaceOption; return InterfaceOption;
}, },
volumeModeOptions() {
return Object.values(VOLUME_MODE);
},
imageOption() { imageOption() {
return sortBy( return sortBy(
this.images this.images
@@ -185,25 +177,6 @@ export default {
return this.$store.getters[`${ inStore }/all`](STORAGE_CLASS); 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() { storageClassOptions() {
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => { return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name; const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
@@ -262,43 +235,7 @@ 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: { 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() { getAccessMode() {
if (!this.longhornV2LVMSupport) { if (!this.longhornV2LVMSupport) {
return ['ReadWriteMany']; return ['ReadWriteMany'];
@@ -307,10 +244,10 @@ export default {
const storageClassName = this.value.spec.storageClassName; const storageClassName = this.value.spec.storageClassName;
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName); const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
let readWriteOnce = this.value.isLvm || (!this.value.thirdPartyStorageFeatureEnabled && this.value.isLonghornV2); let readWriteOnce = this.value.isLvm || this.value.isLonghornV2;
if (storageClass) { if (storageClass) {
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.value.thirdPartyStorageFeatureEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2); readWriteOnce = storageClass.provisioner === LVM_DRIVER || storageClass.parameters?.dataEngine === DATA_ENGINE_V2;
} }
return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany']; return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany'];
@@ -351,7 +288,7 @@ export default {
const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize); const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize);
if (imageSize) { if (imageSize) {
this.storage = `${ Math.ceil(imageSize / 1024 / 1024 / 1024) }${ GIBIBYTE }`; this.storage = `${ Math.ceil(imageSize / 1024 / 1024 / 1024) }Gi`;
} }
} }
this.update(); this.update();
@@ -420,7 +357,7 @@ export default {
/> />
<LabeledSelect <LabeledSelect
v-if="isBlank" v-if="source === 'blank'"
v-model:value="value.spec.storageClassName" v-model:value="value.spec.storageClassName"
:options="storageClassOptions" :options="storageClassOptions"
:label="t('harvester.storage.storageClass.label')" :label="t('harvester.storage.storageClass.label')"
@@ -430,18 +367,6 @@ export default {
@update:value="update" @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 <UnitInput
v-model:value="storage" v-model:value="storage"
:label="t('harvester.volume.size')" :label="t('harvester.volume.size')"
@@ -452,7 +377,6 @@ export default {
:disabled="value?.isLonghornV2 && isEdit" :disabled="value?.isLonghornV2 && isEdit"
required required
class="mb-20" class="mb-20"
:suffix="GIBIBYTE"
@update:value="update" @update:value="update"
/> />
@@ -1,7 +1,6 @@
<script> <script>
import UnitInput from '@shell/components/form/UnitInput'; import UnitInput from '@shell/components/form/UnitInput';
import InputOrDisplay from '@shell/components/InputOrDisplay'; import InputOrDisplay from '@shell/components/InputOrDisplay';
import { GIBIBYTE } from '../../utils/unit';
export default { export default {
name: 'HarvesterEditCpuMemory', name: 'HarvesterEditCpuMemory',
@@ -31,7 +30,6 @@ export default {
data() { data() {
return { return {
GIBIBYTE,
localCpu: this.cpu, localCpu: this.cpu,
localMemory: this.memory localMemory: this.memory
}; };
@@ -65,7 +63,7 @@ export default {
if (String(this.localMemory).includes('Gi')) { if (String(this.localMemory).includes('Gi')) {
memory = this.localMemory; memory = this.localMemory;
} else { } else {
memory = `${ this.localMemory }${ GIBIBYTE }`; memory = `${ this.localMemory }Gi`;
} }
if (memory.includes('null')) { if (memory.includes('null')) {
memory = null; memory = null;
@@ -115,7 +113,6 @@ export default {
:output-modifier="true" :output-modifier="true"
:disabled="disabled" :disabled="disabled"
required required
:suffix="GIBIBYTE"
class="mb-20" class="mb-20"
@update:value="change" @update:value="change"
/> />
@@ -97,7 +97,7 @@ export default {
}); });
const devices = [ const devices = [
...this.otherDevices(this.value?.domain?.devices?.hostDevices || []), ...this.otherDevices(this.value.domain.devices.hostDevices || []),
...formatted, ...formatted,
]; ];
@@ -121,9 +121,7 @@ export default {
return inUse; return inUse;
} }
const hostDevices = vm?.hostDevices || []; vm.hostDevices.forEach((device) => {
hostDevices.forEach((device) => {
inUse[device.name] = { usedBy: [vm.metadata.name] }; inUse[device.name] = { usedBy: [vm.metadata.name] };
}); });
@@ -1,6 +1,6 @@
<script> <script>
import UnitInput from '@shell/components/form/UnitInput'; import UnitInput from '@shell/components/form/UnitInput';
import { MEBIBYTE } from '../../utils/unit';
export default { export default {
name: 'HarvesterReserved', name: 'HarvesterReserved',
@@ -20,7 +20,7 @@ export default {
}, },
data() { data() {
return { MEBIBYTE, memory: this.reservedMemory }; return { memory: this.reservedMemory };
}, },
watch: { watch: {
@@ -47,7 +47,6 @@ export default {
:input-exponent="2" :input-exponent="2"
:increment="1024" :increment="1024"
:output-modifier="true" :output-modifier="true"
:suffix="MEBIBYTE"
@update:value="change" @update:value="change"
/> />
</template> </template>
@@ -1,13 +1,11 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { randomStr } from '@shell/utils/string'; import { randomStr } from '@shell/utils/string';
import { clone } from '@shell/utils/object';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import ModalWithCard from '@shell/components/ModalWithCard'; import ModalWithCard from '@shell/components/ModalWithCard';
import { _VIEW } from '@shell/config/query-params'; import { _VIEW } from '@shell/config/query-params';
import { NAMESPACE } from '@shell/config/types'; import { NAMESPACE } from '@shell/config/types';
import { HCI } from '../../types'; import { HCI } from '../../types';
@@ -128,6 +126,7 @@ export default {
checkedSsh(val) { checkedSsh(val) {
// if click on Create a New... // if click on Create a New...
if (val.includes(_NEW)) { if (val.includes(_NEW)) {
this.checkedSsh = this.checkedSsh.filter((key) => key !== _NEW);
this.show(); this.show();
} }
} }
@@ -231,9 +230,7 @@ export default {
}, },
update() { update() {
const sshKeys = this.checkedSsh.filter((key) => key !== _NEW); this.$emit('update:sshKey', clone(this.checkedSsh));
this.$emit('update:sshKey', sshKeys);
}, },
} }
}; };
@@ -80,7 +80,7 @@ export default {
}); });
const devices = [ const devices = [
...this.otherDevices(this.value?.domain?.devices?.hostDevices || []), ...this.otherDevices(this.value.domain.devices.hostDevices || []),
...formatted, ...formatted,
]; ];
@@ -121,9 +121,7 @@ export default {
return inUse; return inUse;
} }
const hostDevices = vm?.hostDevices || []; vm.hostDevices.forEach((device) => {
hostDevices.forEach((device) => {
inUse[device.name] = { usedBy: [vm.metadata.name] }; inUse[device.name] = { usedBy: [vm.metadata.name] };
}); });
@@ -7,7 +7,8 @@ import UnitInput from '@shell/components/form/UnitInput';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import ModalWithCard from '@shell/components/ModalWithCard'; import ModalWithCard from '@shell/components/ModalWithCard';
import { PVC, STORAGE_CLASS } from '@shell/config/types';
import { PVC } from '@shell/config/types';
import { clone } from '@shell/utils/object'; import { clone } from '@shell/utils/object';
import { ucFirst, randomStr } from '@shell/utils/string'; import { ucFirst, randomStr } from '@shell/utils/string';
import { removeObject } from '@shell/utils/array'; import { removeObject } from '@shell/utils/array';
@@ -16,7 +17,6 @@ import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
import { SOURCE_TYPE } from '../../../config/harvester-map'; import { SOURCE_TYPE } from '../../../config/harvester-map';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester'; import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
import { HCI } from '../../../types'; import { HCI } from '../../../types';
import { VOLUME_MODE } from '@pkg/harvester/config/types';
export default { export default {
emits: ['update:value'], emits: ['update:value'],
@@ -62,7 +62,7 @@ export default {
customVolumeMode: { customVolumeMode: {
type: String, type: String,
default: VOLUME_MODE.Block default: 'Block'
}, },
customAccessMode: { customAccessMode: {
@@ -116,12 +116,6 @@ export default {
return this.mode === _CREATE; return this.mode === _CREATE;
}, },
defaultStorageClass() {
const defaultStorage = this.$store.getters['harvester/all'](STORAGE_CLASS).find((sc) => sc.isDefault);
return defaultStorage;
},
showVolumeTip() { showVolumeTip() {
const imageName = this.getImageDisplayName(this.rows[0]?.image); const imageName = this.getImageDisplayName(this.rows[0]?.image);
@@ -11,7 +11,6 @@ import { _CREATE } from '@shell/config/query-params';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { HCI } from '../../../../types'; import { HCI } from '../../../../types';
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map'; import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
import { GIBIBYTE } from '../../../../utils/unit';
export default { export default {
name: 'HarvesterEditExisting', name: 'HarvesterEditExisting',
@@ -60,7 +59,6 @@ export default {
} }
return { return {
GIBIBYTE,
VOLUME_TYPE, VOLUME_TYPE,
InterfaceOption, InterfaceOption,
loading: false, loading: false,
@@ -111,10 +109,6 @@ export default {
return false; return false;
} }
if (pvc.isGoldenImageVolume) {
return false;
}
if (pvc.attachVM && isAvailable && pvc.attachVM?.id === this.vm?.id && this.isEdit) { if (pvc.attachVM && isAvailable && pvc.attachVM?.id === this.vm?.id && this.isEdit) {
isBeingUsed = false; isBeingUsed = false;
} else if (pvc.attachVM) { } else if (pvc.attachVM) {
@@ -142,12 +136,10 @@ export default {
return; return;
} }
// update this.value with existing volume spec, then update to upstream component
this.value.accessModes = pvcResource.spec.accessModes[0]; this.value.accessModes = pvcResource.spec.accessModes[0];
this.value.size = pvcResource.spec.resources.requests.storage; this.value.size = pvcResource.spec.resources.requests.storage;
this.value.storageClassName = pvcResource.spec.storageClassName; this.value.storageClassName = pvcResource.spec.storageClassName;
this.value.volumeMode = pvcResource.spec.volumeMode; this.value.volumeMode = pvcResource.spec.volumeMode;
this.update();
}, },
'value.type'(neu) { 'value.type'(neu) {
@@ -258,7 +250,6 @@ export default {
:label="t('harvester.fields.size')" :label="t('harvester.fields.size')"
:mode="mode" :mode="mode"
:disabled="true" :disabled="true"
:suffix="GIBIBYTE"
@update:value="update" @update:value="update"
/> />
</InputOrDisplay> </InputOrDisplay>
@@ -12,7 +12,6 @@ import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
import { _VIEW } from '@shell/config/query-params'; import { _VIEW } from '@shell/config/query-params';
import LabelValue from '@shell/components/LabelValue'; import LabelValue from '@shell/components/LabelValue';
import { ucFirst } from '@shell/utils/string'; import { ucFirst } from '@shell/utils/string';
import { GIBIBYTE } from '../../../../utils/unit';
export default { export default {
name: 'HarvesterEditVMImage', name: 'HarvesterEditVMImage',
@@ -68,7 +67,6 @@ export default {
data() { data() {
return { return {
GIBIBYTE,
VOLUME_TYPE, VOLUME_TYPE,
InterfaceOption, InterfaceOption,
loading: false, loading: false,
@@ -92,7 +90,7 @@ export default {
imagesOption() { imagesOption() {
return this.images.filter((c) => c.isReady).sort((a, b) => a.creationTimestamp > b.creationTimestamp ? -1 : 1).map( (I) => { return this.images.filter((c) => c.isReady).sort((a, b) => a.creationTimestamp > b.creationTimestamp ? -1 : 1).map( (I) => {
return { return {
label: this.imageOptionLabel(I), label: `${ I.metadata.namespace }/${ I.spec.displayName }`,
value: I.id value: I.id
}; };
}); });
@@ -118,46 +116,8 @@ export default {
}); });
}, },
thirdPartyStorageEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
},
isLonghornV2() { isLonghornV2() {
return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.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;
} }
}, },
@@ -189,7 +149,7 @@ export default {
minExponent: 3, minExponent: 3,
}); });
this.value.size = `${ formatSize }${ GIBIBYTE }`; this.value.size = `${ formatSize }Gi`;
} }
}, },
deep: true, deep: true,
@@ -198,17 +158,7 @@ export default {
}, },
methods: { methods: {
imageOptionLabel(image) {
let label = `${ image.metadata.namespace }/${ image.spec.displayName }`;
if (this.thirdPartyStorageEnabled) {
label += ` (${ image.imageStorageClass } / ${ image.virtualSize })`;
}
return label;
},
update() { update() {
this.value.hasDiskError = this.showDiskTooSmallError;
this.$emit('update'); this.$emit('update');
}, },
@@ -231,7 +181,7 @@ export default {
if (!isIsoImage) { if (!isIsoImage) {
imageSizeGiB = Math.max(imageSizeGiB, 10); imageSizeGiB = Math.max(imageSizeGiB, 10);
} }
this.value['size'] = `${ imageSizeGiB }${ GIBIBYTE }`; this.value['size'] = `${ imageSizeGiB }Gi`;
} }
this.update(); this.update();
@@ -338,7 +288,7 @@ export default {
:mode="mode" :mode="mode"
:required="validateRequired" :required="validateRequired"
:disable="isLonghornV2" :disable="isLonghornV2"
:suffix="GIBIBYTE" suffix="GiB"
@update:value="update" @update:value="update"
/> />
</InputOrDisplay> </InputOrDisplay>
@@ -391,10 +341,5 @@ export default {
class="mb-20" class="mb-20"
:label="value.volumeBackups.error.message" :label="value.volumeBackups.error.message"
/> />
<Banner
v-if="!isView && showDiskTooSmallError"
color="error"
:label="t('harvester.virtualMachine.volume.vmImageVolumeTip', {diskSize: diskSize, imageVirtualSize: imageVirtualSize})"
/>
</div> </div>
</template> </template>
@@ -12,8 +12,6 @@ import LabelValue from '@shell/components/LabelValue';
import { ucFirst } from '@shell/utils/string'; import { ucFirst } from '@shell/utils/string';
import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storageclass'; import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storageclass';
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim'; import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
import { GIBIBYTE } from '../../../../utils/unit';
import { VOLUME_MODE } from '@pkg/harvester/config/types';
export default { export default {
name: 'HarvesterEditVolume', name: 'HarvesterEditVolume',
@@ -60,7 +58,6 @@ export default {
data() { data() {
return { return {
GIBIBYTE,
VOLUME_TYPE, VOLUME_TYPE,
InterfaceOption, InterfaceOption,
loading: false, loading: false,
@@ -72,10 +69,6 @@ export default {
return this.$store.getters['harvester-common/getFeatureEnabled']('longhornV2LVMSupport'); return this.$store.getters['harvester-common/getFeatureEnabled']('longhornV2LVMSupport');
}, },
thirdPartyStorageClassEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
},
encryptionValue() { encryptionValue() {
return ucFirst(String(this.value.isEncrypted)); return ucFirst(String(this.value.isEncrypted));
}, },
@@ -96,18 +89,6 @@ export default {
return allPVCs.find((P) => P.id === `${ this.namespace }/${ this.value.volumeName }`); 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() { isDisabled() {
return !this.value.newCreateId && this.isEdit && this.isVirtualType; return !this.value.newCreateId && this.isEdit && this.isVirtualType;
}, },
@@ -129,17 +110,7 @@ export default {
isLonghornV2() { isLonghornV2() {
return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.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: { watch: {
@@ -147,8 +118,6 @@ export default {
immediate: true, immediate: true,
handler(neu) { handler(neu) {
this.value.accessMode = this.getAccessMode(neu); this.value.accessMode = this.getAccessMode(neu);
this.value.volumeMode = this.getVolumeMode(neu, this.value.volumeMode);
this.update();
} }
}, },
@@ -171,7 +140,7 @@ export default {
minExponent: 3, minExponent: 3,
}); });
this.value.size = `${ formatSize }${ GIBIBYTE }`; this.value.size = `${ formatSize }Gi`;
} }
}, },
deep: true, deep: true,
@@ -180,20 +149,6 @@ export default {
}, },
methods: { 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) { getAccessMode(storageClassName) {
if (!this.longhornV2LVMSupport) { if (!this.longhornV2LVMSupport) {
return 'ReadWriteMany'; return 'ReadWriteMany';
@@ -201,10 +156,10 @@ export default {
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName); const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
let readWriteOnce = this.value.pvc?.isLvm || (!this.thirdPartyStorageClassEnabled && this.value.pvc?.isLonghornV2); let readWriteOnce = this.value.pvc?.isLvm || this.value.pvc?.isLonghornV2;
if (storageClass) { if (storageClass) {
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.thirdPartyStorageClassEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2); readWriteOnce = storageClass.provisioner === LVM_DRIVER || storageClass.parameters?.dataEngine === DATA_ENGINE_V2;
} }
return readWriteOnce ? 'ReadWriteOnce' : 'ReadWriteMany'; return readWriteOnce ? 'ReadWriteOnce' : 'ReadWriteMany';
@@ -303,7 +258,6 @@ export default {
:required="validateRequired" :required="validateRequired"
:label="t('harvester.fields.size')" :label="t('harvester.fields.size')"
:disabled="isLonghornV2" :disabled="isLonghornV2"
:suffix="GIBIBYTE"
@update:value="update" @update:value="update"
/> />
</InputOrDisplay> </InputOrDisplay>
@@ -329,29 +283,6 @@ export default {
/> />
</InputOrDisplay> </InputOrDisplay>
</div> </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 <div
v-if="value.volumeEncryptionFeatureEnabled && isView" v-if="value.volumeEncryptionFeatureEnabled && isView"
class="col span-6" class="col span-6"
@@ -361,9 +292,11 @@ export default {
:value="encryptionValue" :value="encryptionValue"
/> />
</div> </div>
</div>
<div class="row mb-20">
<div <div
v-if="value.volumeBackups && isView" v-if="value.volumeBackups && isView"
class="col span-6" class="col span-3"
> >
<LabelValue <LabelValue
:name="t('harvester.virtualMachine.volume.readyToUse')" :name="t('harvester.virtualMachine.volume.readyToUse')"
@@ -2,7 +2,6 @@
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import Tabbed from '@shell/components/Tabbed'; import Tabbed from '@shell/components/Tabbed';
import { clone, set } from '@shell/utils/object';
import Tab from '@shell/components/Tabbed/Tab'; import Tab from '@shell/components/Tabbed/Tab';
import { Checkbox } from '@components/Form/Checkbox'; import { Checkbox } from '@components/Form/Checkbox';
import CruResource from '@shell/components/CruResource'; import CruResource from '@shell/components/CruResource';
@@ -11,12 +10,16 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import NameNsDescription from '@shell/components/form/NameNsDescription'; import NameNsDescription from '@shell/components/form/NameNsDescription';
import UnitInput from '@shell/components/form/UnitInput'; import UnitInput from '@shell/components/form/UnitInput';
import Labels from '@shell/components/form/Labels';
import NodeScheduling from '@shell/components/form/NodeScheduling'; import NodeScheduling from '@shell/components/form/NodeScheduling';
import PodAffinity from '@shell/components/form/PodAffinity'; import PodAffinity from '@shell/components/form/PodAffinity';
import VGpuDevices from './VirtualMachineVGpuDevices/index'; import VGpuDevices from './VirtualMachineVGpuDevices/index';
import UsbDevices from './VirtualMachineUSBDevices/index'; import UsbDevices from './VirtualMachineUSBDevices/index';
import KeyValue from '@shell/components/form/KeyValue'; import KeyValue from '@shell/components/form/KeyValue';
import { clear } from '@shell/utils/array'; import { clear } from '@shell/utils/array';
import { clone } from '@shell/utils/object';
import { saferDump } from '@shell/utils/create-yaml'; import { saferDump } from '@shell/utils/create-yaml';
import { exceptionToErrorsArray } from '@shell/utils/error'; import { exceptionToErrorsArray } from '@shell/utils/error';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
@@ -62,6 +65,7 @@ export default {
PodAffinity, PodAffinity,
AccessCredentials, AccessCredentials,
Reserved, Reserved,
Labels,
PciDevices, PciDevices,
RestartVMDialog, RestartVMDialog,
UnitInput, UnitInput,
@@ -191,12 +195,7 @@ export default {
return true; return true;
}, },
isValidationPassed() {
// check if any disk hasDiskError is true
const hasError = this.diskRows.some((disk) => disk.hasDiskError === true);
return !hasError;
},
showCpuPinningBanner() { showCpuPinningBanner() {
if (!this.value.cpuPinningFeatureEnabled) { if (!this.value.cpuPinningFeatureEnabled) {
return false; return false;
@@ -211,10 +210,7 @@ export default {
} }
return false; return false;
}, }
usbPassthroughEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
},
}, },
watch: { watch: {
@@ -411,26 +407,13 @@ export default {
} }
const cloneDeepNewVM = clone(this.value); const cloneDeepNewVM = clone(this.value);
// new VM
delete cloneDeepNewVM?.metadata; delete cloneDeepNewVM?.metadata;
delete cloneDeepNewVM?.__clone;
// old VM
delete this.cloneVM?.metadata; delete this.cloneVM?.metadata;
delete this.cloneVM?.__clone; 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 oldVM = JSON.parse(JSON.stringify(this.cloneVM));
const newVM = JSON.parse(JSON.stringify(cloneDeepNewVM)); 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)) { if (isEqual(oldVM, newVM)) {
return; return;
} }
@@ -439,11 +422,7 @@ export default {
this.isOpen = true; this.isOpen = true;
this.$nextTick(() => { this.$nextTick(() => {
if (this?.$refs?.restartDialog) { this.$refs.restartDialog.resolve = resolve;
this.$refs.restartDialog.resolve = resolve;
} else {
return resolve();
}
}); });
}); });
}, },
@@ -493,7 +472,6 @@ export default {
}, },
generateYaml() { generateYaml() {
this.showYaml = this?.$refs?.vmCruResource?.showYaml || false;
this.parseVM(); this.parseVM();
const out = saferDump(this.value); const out = saferDump(this.value);
@@ -507,12 +485,10 @@ export default {
<CruResource <CruResource
v-if="spec" v-if="spec"
id="vm" id="vm"
ref="vmCruResource"
:done-route="doneRoute" :done-route="doneRoute"
:resource="value" :resource="value"
:cancel-event="true" :cancel-event="true"
:mode="mode" :mode="mode"
:validation-passed="isValidationPassed"
:can-yaml="isSingle ? true : false" :can-yaml="isSingle ? true : false"
:errors="errors" :errors="errors"
:generate-yaml="generateYaml" :generate-yaml="generateYaml"
@@ -695,7 +671,7 @@ export default {
</Tab> </Tab>
<Tab <Tab
v-if="enabledPCI && usbPassthroughEnabled" v-if="enabledPCI"
:label="t('harvester.tab.usbDevices')" :label="t('harvester.tab.usbDevices')"
name="usbDevices" name="usbDevices"
:weight="-7" :weight="-7"
@@ -721,50 +697,39 @@ export default {
/> />
</Tab> </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 <Tab
name="instanceLabel" name="instanceLabel"
:label="t('harvester.tab.instanceLabel')" :label="t('harvester.tab.instanceLabel')"
:weight="-10" :weight="-8"
> >
<Banner color="info"> <Labels
<t k="harvester.virtualMachine.instanceLabels.banner" /> :default-container-class="'labels-and-annotations-container'"
</Banner> :value="value"
<KeyValue
key="instance-labels"
:value="value.instanceLabels"
:protected-keys="value.systemLabels || []"
:toggle-filter="toggler"
:add-label="t('labels.addLabel')"
:mode="mode" :mode="mode"
:read-allowed="false" :display-side-by-side="false"
:value-can-be-empty="true" :show-annotations="false"
@update:value="value.setInstanceLabels($event)" :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>
</Tab> </Tab>
<Tab <Tab
name="advanced" name="advanced"
:label="t('harvester.tab.advanced')" :label="t('harvester.tab.advanced')"
:weight="-11" :weight="-9"
> >
<div class="row mb-20"> <div class="row mb-20">
<div class="col span-6"> <div class="col span-6">
@@ -908,29 +873,11 @@ export default {
:mode="mode" :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 <Checkbox
v-model:value="efiEnabled" v-model:value="efiEnabled"
class="check" class="check"
type="checkbox" type="checkbox"
:label="t('harvester.virtualMachine.advancedOptions.efiEnabled')" :label="t('harvester.virtualMachine.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" :mode="mode"
/> />
@@ -939,10 +886,9 @@ export default {
v-model:value="secureBoot" v-model:value="secureBoot"
class="check" class="check"
type="checkbox" type="checkbox"
:label="t('harvester.virtualMachine.advancedOptions.secureBoot')" :label="t('harvester.virtualMachine.secureBoot')"
:mode="mode" :mode="mode"
/> />
<Banner <Banner
v-if="showCpuPinningBanner" v-if="showCpuPinningBanner"
color="warning" color="warning"
@@ -2,7 +2,7 @@
import ConsumptionGauge from '@shell/components/ConsumptionGauge'; import ConsumptionGauge from '@shell/components/ConsumptionGauge';
import { METRIC, NODE } from '@shell/config/types'; import { METRIC, NODE } from '@shell/config/types';
import { formatSi, exponentNeeded, UNITS, parseSi } from '@shell/utils/units'; import { formatSi, exponentNeeded, UNITS, parseSi } from '@shell/utils/units';
import { UNIT_SUFFIX } from '../utils/unit';
export default { export default {
name: 'HarvesterMemoryUsed', name: 'HarvesterMemoryUsed',
components: { ConsumptionGauge }, components: { ConsumptionGauge },
@@ -51,7 +51,7 @@ export default {
memoryUnits() { memoryUnits() {
const exponent = exponentNeeded(this.memoryTotal, 1024); const exponent = exponentNeeded(this.memoryTotal, 1024);
return `${ UNITS[exponent] }${ UNIT_SUFFIX }`; return `${ UNITS[exponent] }iB`;
}, },
node() { node() {
@@ -2,7 +2,6 @@
import ConsumptionGauge from '@shell/components/ConsumptionGauge'; import ConsumptionGauge from '@shell/components/ConsumptionGauge';
import { LONGHORN } from '@shell/config/types'; import { LONGHORN } from '@shell/config/types';
import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units'; import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
import { UNIT_SUFFIX } from '../utils/unit';
export default { export default {
name: 'HarvesterStorageUsed', name: 'HarvesterStorageUsed',
@@ -76,13 +75,13 @@ export default {
allocatedUnits() { allocatedUnits() {
const exponent = exponentNeeded(this.storageStats.total, 1024); const exponent = exponentNeeded(this.storageStats.total, 1024);
return `${ UNITS[exponent] }${ UNIT_SUFFIX }`; return `${ UNITS[exponent] }iB`;
}, },
usedUnits() { usedUnits() {
const exponent = exponentNeeded(this.storageStats.maximum, 1024); const exponent = exponentNeeded(this.storageStats.maximum, 1024);
return `${ UNITS[exponent] }${ UNIT_SUFFIX }`; return `${ UNITS[exponent] }iB`;
}, },
formatUsed() { formatUsed() {
+5 -54
View File
@@ -38,26 +38,6 @@ asyncButton:
success: Restarted success: Restarted
waiting: Restarting&hellip; waiting: Restarting&hellip;
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: harvester:
branding: branding:
logos: logos:
@@ -66,7 +46,6 @@ harvester:
tip: 'Upload an icon to replace the Harvester favicon in the browser tab. Max file size is 20KB' tip: 'Upload an icon to replace the Harvester favicon in the browser tab. Max file size is 20KB'
productLabel: 'Harvester' productLabel: 'Harvester'
modal: modal:
backup: backup:
success: 'Backup { backUpName } has been initiated.' success: 'Backup { backUpName } has been initiated.'
addBackup: Add Backup addBackup: Add Backup
@@ -105,14 +84,6 @@ harvester:
tip: Please enter a virtual machine name! tip: Please enter a virtual machine name!
success: 'Virtual machine { name } cloned successfully.' success: 'Virtual machine { name } cloned successfully.'
failed: 'Failed clone virtual machine!' 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: exportImage:
title: Export to Image title: Export to Image
name: Name name: Name
@@ -613,17 +584,15 @@ harvester:
enableUsb: Enable USB Tablet enableUsb: Enable USB Tablet
advancedOptions: advancedOptions:
tpm: Enable TPM tpm: Enable TPM
tpmPersistentState: TPM Persistent State
cpuManager: cpuManager:
prefix: You must enable CPU Manager for at least one node in prefix: You must enable CPU Manager for at least one node in
middle: 'host page' middle: 'host page'
suffix: to enable CPU Pinning for VM 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. usbTip: Provides an absolute pointer device which often helps with getting a consistent mouse cursor position in VNC.
sshTitle: Add Public SSH Key sshTitle: Add Public SSH Key
imageTip: An external URL to the .iso, .img, .qcow2 or .raw that the virtual machine should be created from. 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: volume:
dragTip: Drag and drop volumes, or use the volume's arrows, to change the boot order. 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. volumeTip: The virtual machine only contains a CD-ROM volume. You may want to add additional disk volumes.
@@ -644,7 +613,6 @@ harvester:
setFirst: Set as root volume setFirst: Set as root volume
saveVolume: Update Volume saveVolume: Update Volume
encryption: Encryption encryption: Encryption
vmImageVolumeTip: Disk size ({diskSize}) should greater than selected image virtual size ({imageVirtualSize})
lockTooltip: lockTooltip:
all: All volumes are encrypted. all: All volumes are encrypted.
partial: Some volumes are encrypted. partial: Some volumes are encrypted.
@@ -787,10 +755,6 @@ harvester:
inNamespaces: "Workloads in these namespaces" inNamespaces: "Workloads in these namespaces"
namespaces: namespaces:
label: 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: volume:
label: Volumes label: Volumes
@@ -800,7 +764,6 @@ harvester:
datasource: Data Source datasource: Data Source
details: Details details: Details
size: Size size: Size
volumeMode: Volume Mode
source: Source source: Source
kind: Kind kind: Kind
sourceOptions: sourceOptions:
@@ -851,8 +814,6 @@ harvester:
=1 {1 image is uploading, please do not refresh or close the page.} =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.} 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 checksum: Checksum
checksumTip: Validate the image using the SHA512 checksum, if specified. checksumTip: Validate the image using the SHA512 checksum, if specified.
@@ -877,22 +838,17 @@ harvester:
currentVersion: CURRENT VERSION currentVersion: CURRENT VERSION
versionLabel: AVAILABLE COMPATIBLE VERSIONS versionLabel: AVAILABLE COMPATIBLE VERSIONS
enableLogging: Enable Logging enableLogging: Enable Logging
skipSingleReplicaDetachedVol: Skip checking single-replica detached volumes
upgradeNode: Upgrading Node upgradeNode: Upgrading Node
upgradeSysService: Upgrading System Service upgradeSysService: Upgrading System Service
upgradeImage: Download Upgrade Image upgradeImage: Download Upgrade Image
osUpgrade: Cluster Upgrade osUpgrade: OS Upgrade
uploadNew: Upload New Image uploadNew: Upload New Image
deleteHeader: Please select an image to delete.
deleteExisting: Delete Existing Image
selectExisting: Select Existing Image selectExisting: Select Existing Image
createRepository: Creating Upgrade Repository createRepository: Creating Upgrade Repository
succeeded: Succeeded succeeded: Succeeded
releaseTip: Please read the upgrade documentation carefully. You can view details on the <a href="{url}" target="_blank">Harvester Release Notes</a>. 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. checkReady: I have read and understood the upgrade instructions related to this Harvester version.
pending: Pending pending: Pending
upload:
duplicatedFile: The file you are trying to upload already exists.
repoInfo: repoInfo:
upgradeStatus: Upgrade Status upgradeStatus: Upgrade Status
os: OS os: OS
@@ -1058,7 +1014,7 @@ harvester:
invalid: '"Exclude list" is invalid.' invalid: '"Exclude list" is invalid.'
addIp: Add Exclude IP addIp: Add Exclude IP
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.' warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.' tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 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>.'
vmForceDeletionPolicy: vmForceDeletionPolicy:
period: Period period: Period
ratio : Ratio ratio : Ratio
@@ -1095,15 +1051,10 @@ harvester:
addConfig: Add Configuration addConfig: Add Configuration
upgrade: upgrade:
unknownImageName: Image name is not found. selectExitImage: Please select the OS image to upgrade.
imageUrl: Please input a valid image URL. imageUrl: Please input a valid image URL.
chooseFile: Please select to upload an image. chooseFile: Please select to upload an image.
checksum: Checksum 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: harvesterMonitoring:
label: Harvester Monitoring label: Harvester Monitoring
section: section:
@@ -8,7 +8,9 @@ import Banner from '@components/Banner/Banner.vue';
import MessageLink from '@shell/components/MessageLink'; import MessageLink from '@shell/components/MessageLink';
import SortableTable from '@shell/components/SortableTable'; import SortableTable from '@shell/components/SortableTable';
import { allHash, setPromiseResult } from '@shell/utils/promise'; import { allHash, setPromiseResult } from '@shell/utils/promise';
import { parseSi, formatSi, exponentNeeded, UNITS } from '@shell/utils/units'; import {
parseSi, formatSi, exponentNeeded, UNITS, createMemoryValues
} from '@shell/utils/units';
import { REASON } from '@shell/config/table-headers'; import { REASON } from '@shell/config/table-headers';
import { import {
EVENT, METRIC, NODE, SERVICE, PVC, LONGHORN, POD, COUNT, NETWORK_ATTACHMENT EVENT, METRIC, NODE, SERVICE, PVC, LONGHORN, POD, COUNT, NETWORK_ATTACHMENT
@@ -25,21 +27,22 @@ import { isEmpty } from '@shell/utils/object';
import { HCI } from '../types'; import { HCI } from '../types';
import HarvesterUpgrade from '../components/HarvesterUpgrade'; import HarvesterUpgrade from '../components/HarvesterUpgrade';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester'; import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
import { UNIT_SUFFIX } from '../utils/unit';
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(minMax); dayjs.extend(minMax);
const PARSE_RULES = { const PARSE_RULES = {
format: { memory: {
addSuffix: true, format: {
firstSuffix: UNIT_SUFFIX, addSuffix: true,
increment: 1024, firstSuffix: 'B',
maxExponent: 99, increment: 1024,
maxPrecision: 2, maxExponent: 99,
minExponent: 0, maxPrecision: 2,
startingExponent: 0, minExponent: 0,
suffix: UNIT_SUFFIX, startingExponent: 0,
suffix: 'iB',
}
} }
}; };
@@ -132,7 +135,6 @@ export default {
const hash = { const hash = {
vms: this.fetchClusterResources(HCI.VM), vms: this.fetchClusterResources(HCI.VM),
pvcs: this.fetchClusterResources(PVC),
nodes: this.fetchClusterResources(NODE), nodes: this.fetchClusterResources(NODE),
events: this.fetchClusterResources(EVENT), events: this.fetchClusterResources(EVENT),
metricNodes: this.fetchClusterResources(METRIC.NODE), metricNodes: this.fetchClusterResources(METRIC.NODE),
@@ -222,7 +224,6 @@ export default {
nodes: [], nodes: [],
metricNodes: [], metricNodes: [],
vms: [], vms: [],
pvcs: [],
monitoring: {}, monitoring: {},
VM_DASHBOARD_METRICS_URL, VM_DASHBOARD_METRICS_URL,
CLUSTER_METRICS_SUMMARY_URL, CLUSTER_METRICS_SUMMARY_URL,
@@ -287,14 +288,6 @@ export default {
out[resource.type].name = this.t(`typeLabel."${ resource.spoofed.name }"`, { count: out[resource.type].total }); 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) { if (resource.type === HCI.BLOCK_DEVICE) {
let total = 0; let total = 0;
let errorCount = 0; let errorCount = 0;
@@ -409,13 +402,13 @@ export default {
storageUsed() { storageUsed() {
const stats = this.storageStats; const stats = this.storageStats;
return this.createDisplayValues(stats.maximum, stats.used); return this.createMemoryValues(stats.maximum, stats.used);
}, },
storageAllocated() { storageAllocated() {
const stats = this.storageStats; const stats = this.storageStats;
return this.createDisplayValues(stats.total, stats.scheduled); return this.createMemoryValues(stats.total, stats.scheduled);
}, },
vmEvents() { vmEvents() {
@@ -461,7 +454,7 @@ export default {
return total + node.memoryReserved; return total + node.memoryReserved;
}, 0); }, 0);
return this.createDisplayValues(this.memoryTotal, useful); return createMemoryValues(this.memoryTotal, useful);
}, },
availableNodes() { availableNodes() {
@@ -501,7 +494,7 @@ export default {
}, },
ramUsed() { ramUsed() {
return this.createDisplayValues(this.memoryTotal, this.metricAggregations?.memory); return createMemoryValues(this.memoryTotal, this.metricAggregations?.memory);
}, },
hasMetricNodeSchema() { hasMetricNodeSchema() {
@@ -523,12 +516,10 @@ export default {
}, },
methods: { methods: {
createDisplayValues(total, useful) { createMemoryValues(total, useful) {
const parsedTotal = parseSi((total || '0').toString()); const parsedTotal = parseSi((total || '0').toString());
const parsedUseful = parseSi((useful || '0').toString()); const parsedUseful = parseSi((useful || '0').toString());
const format = this.createFormat(parsedTotal); const format = this.createMemoryFormat(parsedTotal);
const formattedTotal = formatSi(parsedTotal, format); const formattedTotal = formatSi(parsedTotal, format);
let formattedUseful = formatSi(parsedUseful, { let formattedUseful = formatSi(parsedUseful, {
...format, ...format,
@@ -547,24 +538,24 @@ export default {
useful: Number(parsedUseful), useful: Number(parsedUseful),
formattedTotal, formattedTotal,
formattedUseful, formattedUseful,
units: this.createUnits(parsedTotal), units: this.createMemoryUnits(parsedTotal),
}; };
}, },
createFormat(n) { createMemoryFormat(n) {
const exponent = exponentNeeded(n, PARSE_RULES.format.increment); const exponent = exponentNeeded(n, PARSE_RULES.memory.format.increment);
return { return {
...PARSE_RULES.format, ...PARSE_RULES.memory.format,
maxExponent: exponent, maxExponent: exponent,
minExponent: exponent, minExponent: exponent,
}; };
}, },
createUnits(n) { createMemoryUnits(n) {
const exponent = exponentNeeded(n, PARSE_RULES.format.increment); const exponent = exponentNeeded(n, PARSE_RULES.memory.format.increment);
return `${ UNITS[exponent] }${ PARSE_RULES.format.suffix }`; return `${ UNITS[exponent] }${ PARSE_RULES.memory.format.suffix }`;
}, },
async fetchClusterResources(type, opt = {}, store) { async fetchClusterResources(type, opt = {}, store) {
+2 -2
View File
@@ -101,14 +101,14 @@ export default {
const metricCol = [ const metricCol = [
{ {
name: 'cpu', name: 'cpu',
labelKey: 'harvester.dashboard.hardwareResourceGauge.cpu', labelKey: 'node.detail.glance.consumptionGauge.cpu',
value: 'id', value: 'id',
formatter: 'HarvesterCPUUsed', formatter: 'HarvesterCPUUsed',
formatterOpts: { showUsed: true }, formatterOpts: { showUsed: true },
}, },
{ {
name: 'memory', name: 'memory',
labelKey: 'harvester.dashboard.hardwareResourceGauge.memory', labelKey: 'node.detail.glance.consumptionGauge.memory',
value: 'id', value: 'id',
formatter: 'HarvesterMemoryUsed', formatter: 'HarvesterMemoryUsed',
formatterOpts: { showUsed: true }, formatterOpts: { showUsed: true },
+11 -2
View File
@@ -2,12 +2,13 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import Loading from '@shell/components/Loading'; import Loading from '@shell/components/Loading';
import { VIEW_IN_API, DEV } from '@shell/store/prefs';
import { MANAGEMENT } from '@shell/config/types'; import { MANAGEMENT } from '@shell/config/types';
import { allHash } from '@shell/utils/promise'; import { allHash } from '@shell/utils/promise';
import Tabbed from '@shell/components/Tabbed/index.vue'; import Tabbed from '@shell/components/Tabbed/index.vue';
import Tab from '@shell/components/Tabbed/Tab.vue'; import Tab from '@shell/components/Tabbed/Tab.vue';
import Settings from '@pkg/harvester/components/SettingList.vue'; import Settings from '@pkg/harvester/components/SettingList.vue';
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings'; import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING } from '../config/settings';
import { HCI } from '../types'; import { HCI } from '../types';
export default { export default {
@@ -20,6 +21,14 @@ export default {
}, },
async fetch() { 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 isSingleProduct = !!this.$store.getters['isSingleProduct'];
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
@@ -68,7 +77,7 @@ export default {
}; };
s.hide = s.canHide = (s.kind === 'json' || s.kind === 'multiline' || s.customFormatter === 'json' || s.data.customFormatter === 'json'); s.hide = s.canHide = (s.kind === 'json' || s.kind === 'multiline' || s.customFormatter === 'json' || s.data.customFormatter === 'json');
s.hasActions = s.id === HCI_SETTING.SERVER_VERSION ? true : !s.readOnly; s.hasActions = !s.readOnly || isDev;
initSettings.push(s); initSettings.push(s);
}); });
@@ -6,6 +6,7 @@ import {
} from '@shell/config/types'; } from '@shell/config/types';
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers'; import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
import HarvesterVolumeState from '../formatters/HarvesterVolumeState'; import HarvesterVolumeState from '../formatters/HarvesterVolumeState';
import { allSettled } from '../utils/promise'; import { allSettled } from '../utils/promise';
import { HCI, VOLUME_SNAPSHOT } from '../types'; import { HCI, VOLUME_SNAPSHOT } from '../types';
@@ -57,6 +58,7 @@ export default {
if (!pvcSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) { if (!pvcSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) {
this.$store.dispatch('type-map/configureType', { match: HCI.VOLUME, isCreatable: false }); this.$store.dispatch('type-map/configureType', { match: HCI.VOLUME, isCreatable: false });
} }
this.rows = hash.pvcs; this.rows = hash.pvcs;
}, },
@@ -68,10 +70,7 @@ export default {
schema() { schema() {
return schema; return schema;
}, },
filterRows() {
// we only show the non golden image PVCs in the list
return this.rows.filter((pvc) => !pvc?.isGoldenImageVolume);
},
headers() { headers() {
return [ return [
STATE, STATE,
@@ -93,8 +92,7 @@ export default {
{ {
name: 'storageClass', name: 'storageClass',
labelKey: 'tableHeaders.storageClass', labelKey: 'tableHeaders.storageClass',
value: 'spec.storageClassName', value: 'spec.storageClassName'
sort: 'spec.storageClassName'
}, },
{ {
name: 'AttachedVM', name: 'AttachedVM',
@@ -148,7 +146,7 @@ export default {
:groupable="true" :groupable="true"
default-sort-by="age" default-sort-by="age"
:namespaced="true" :namespaced="true"
:rows="filterRows" :rows="rows"
:schema="schema" :schema="schema"
key-field="_key" key-field="_key"
> >
+1 -9
View File
@@ -137,15 +137,7 @@ export default {
}, },
isTpmEnabled(spec) { 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) { isSecureBoot(spec) {
+30 -57
View File
@@ -25,8 +25,6 @@ import { HCI_SETTING } from '../../config/settings';
import { HCI } from '../../types'; import { HCI } from '../../types';
import { parseVolumeClaimTemplates } from '../../utils/vm'; import { parseVolumeClaimTemplates } from '../../utils/vm';
import impl, { QGA_JSON, USB_TABLET } from './impl'; 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'; const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
@@ -140,7 +138,6 @@ export default {
return { return {
OS, OS,
isClone, isClone,
showYaml: false,
spec: null, spec: null,
osType: 'linux', osType: 'linux',
sshKey: [], sshKey: [],
@@ -167,8 +164,6 @@ export default {
accessCredentials: [], accessCredentials: [],
efiEnabled: false, efiEnabled: false,
tpmEnabled: false, tpmEnabled: false,
tpmPersistentStateEnabled: false,
efiPersistentStateEnabled: false,
secureBoot: false, secureBoot: false,
userDataTemplateId: '', userDataTemplateId: '',
saveUserDataAsClearText: false, saveUserDataAsClearText: false,
@@ -222,7 +217,7 @@ export default {
const selectedVMNetworks = networkNames.map((name) => vmNetworks.find((n) => n.id === name)).filter((n) => n?.id); const selectedVMNetworks = networkNames.map((name) => vmNetworks.find((n) => n.id === name)).filter((n) => n?.id);
const clusterNetworks = uniq(selectedVMNetworks.map((n) => n.clusterNetworkResource?.id)); const clusterNetworks = uniq(selectedVMNetworks.map((n) => n.clusterNetworkResource?.id));
return nodes.filter((N) => !N.isUnSchedulable && N.isEtcd !== 'true').map((node) => { return nodes.filter((N) => !N.isUnSchedulable).map((node) => {
const requireLabelKeys = []; const requireLabelKeys = [];
let isNetworkSchedule = true; let isNetworkSchedule = true;
@@ -246,6 +241,12 @@ export default {
}); });
}, },
defaultStorageClass() {
const defaultStorage = this.$store.getters[`${ this.inStore }/all`](STORAGE_CLASS).find( (O) => O.isDefault);
return defaultStorage;
},
storageClassSetting() { storageClassSetting() {
try { try {
const storageClassValue = this.$store.getters[`${ this.inStore }/all`](HCI.SETTING).find( (O) => O.id === HCI_SETTING.DEFAULT_STORAGE_CLASS)?.value; const storageClassValue = this.$store.getters[`${ this.inStore }/all`](HCI.SETTING).find( (O) => O.id === HCI_SETTING.DEFAULT_STORAGE_CLASS)?.value;
@@ -257,7 +258,7 @@ export default {
}, },
customVolumeMode() { customVolumeMode() {
return this.storageClassSetting.volumeMode || VOLUME_MODE.BLOCK; return this.storageClassSetting.volumeMode || 'Block';
}, },
customAccessMode() { customAccessMode() {
@@ -270,7 +271,7 @@ export default {
needNewSecret() { needNewSecret() {
// When creating a template it is always necessary to create a new secret. // When creating a template it is always necessary to create a new secret.
return this.showYaml ? false : this.resourceType === HCI.VM_VERSION || this.isCreate; return this.resourceType === HCI.VM_VERSION || this.isCreate;
}, },
defaultTerminationSetting() { defaultTerminationSetting() {
@@ -366,8 +367,6 @@ export default {
const installAgent = this.hasInstallAgent(userData, osType, true); const installAgent = this.hasInstallAgent(userData, osType, true);
const efiEnabled = this.isEfiEnabled(spec); const efiEnabled = this.isEfiEnabled(spec);
const tpmEnabled = this.isTpmEnabled(spec); const tpmEnabled = this.isTpmEnabled(spec);
const tpmPersistentStateEnabled = this.isTPMPersistentStateEnabled(spec);
const efiPersistentStateEnabled = this.isEFIPersistentStateEnabled(spec);
const secureBoot = this.isSecureBoot(spec); const secureBoot = this.isSecureBoot(spec);
const cpuPinning = this.isCpuPinning(spec); const cpuPinning = this.isCpuPinning(spec);
@@ -399,9 +398,7 @@ export default {
this['installUSBTablet'] = installUSBTablet; this['installUSBTablet'] = installUSBTablet;
this['efiEnabled'] = efiEnabled; this['efiEnabled'] = efiEnabled;
this['efiPersistentStateEnabled'] = efiPersistentStateEnabled;
this['tpmEnabled'] = tpmEnabled; this['tpmEnabled'] = tpmEnabled;
this['tpmPersistentStateEnabled'] = tpmPersistentStateEnabled;
this['secureBoot'] = secureBoot; this['secureBoot'] = secureBoot;
this['cpuPinning'] = cpuPinning; this['cpuPinning'] = cpuPinning;
@@ -445,7 +442,7 @@ export default {
if (!isIsoImage) { if (!isIsoImage) {
imageSizeGiB = Math.max(imageSizeGiB, 10); imageSizeGiB = Math.max(imageSizeGiB, 10);
} }
size = `${ imageSizeGiB }${ GIBIBYTE }`; size = `${ imageSizeGiB }Gi`;
} }
out.push({ out.push({
@@ -459,7 +456,7 @@ export default {
type, type,
storageClassName: '', storageClassName: '',
image: this.imageId, image: this.imageId,
volumeMode: VOLUME_MODE.BLOCK, volumeMode: 'Block',
isEncrypted, isEncrypted,
volumeBackups, volumeBackups,
}); });
@@ -518,7 +515,7 @@ export default {
accessMode = pvcResource?.spec?.accessModes?.[0] || 'ReadWriteMany'; accessMode = pvcResource?.spec?.accessModes?.[0] || 'ReadWriteMany';
size = pvcResource?.spec?.resources?.requests?.storage || '10Gi'; size = pvcResource?.spec?.resources?.requests?.storage || '10Gi';
storageClassName = pvcResource?.spec?.storageClassName; storageClassName = pvcResource?.spec?.storageClassName;
volumeMode = pvcResource?.spec?.volumeMode || VOLUME_MODE.BLOCK; volumeMode = pvcResource?.spec?.volumeMode || 'Block';
volumeName = pvcResource?.metadata?.name || ''; volumeName = pvcResource?.metadata?.name || '';
} }
@@ -555,7 +552,7 @@ export default {
volumeName, volumeName,
container, container,
accessMode, accessMode,
size: `${ formatSize }${ GIBIBYTE }`, size: `${ formatSize }Gi`,
volumeMode: volumeMode || this.customVolumeMode, volumeMode: volumeMode || this.customVolumeMode,
image, image,
type, type,
@@ -670,7 +667,7 @@ export default {
} }
}); });
if (this.needNewSecret || !this.secretName) { if (!this.secretName || this.needNewSecret) {
this.secretName = this.generateSecretName(this.secretNamePrefix); this.secretName = this.generateSecretName(this.secretNamePrefix);
} }
@@ -872,7 +869,7 @@ export default {
} }
} }
if (out.length === 0 && !!this.spec.template.spec.accessCredentials === false) { if (out.length === 0 && !!this.spec.template.spec.accessCredentials) {
delete this.spec.template.spec.accessCredentials; delete this.spec.template.spec.accessCredentials;
} else { } else {
this.spec.template.spec.accessCredentials = out; this.spec.template.spec.accessCredentials = out;
@@ -972,7 +969,7 @@ export default {
parseVolumeClaimTemplate(R, dataVolumeName) { parseVolumeClaimTemplate(R, dataVolumeName) {
if (!String(R.size).includes('Gi') && R.size) { if (!String(R.size).includes('Gi') && R.size) {
R.size = `${ R.size }${ GIBIBYTE }`; R.size = `${ R.size }Gi`;
} }
const out = { const out = {
@@ -1387,21 +1384,13 @@ export default {
} }
}, },
setBootMethod(boot = { setBootMethod(boot = { efi: false, secureBoot: false }) {
efi: false, secureBoot: false, efiPersistentStateEnabled: false if (boot.efi && boot.secureBoot) {
}) {
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); set(this.spec.template.spec.domain, 'features.smm.enabled', true);
} else { 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);
try { try {
delete this.spec.template.spec.domain.features.smm['enabled']; delete this.spec.template.spec.domain.features.smm['enabled'];
const noKeys = Object.keys(this.spec.template.spec.domain.features.smm).length === 0; const noKeys = Object.keys(this.spec.template.spec.domain.features.smm).length === 0;
@@ -1410,12 +1399,10 @@ export default {
delete this.spec.template.spec.domain.features['smm']; delete this.spec.template.spec.domain.features['smm'];
} }
} catch (e) {} } 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 { } else {
delete this.spec.template.spec.domain.firmware.bootloader.efi['persistent']; delete this.spec.template.spec.domain['firmware'];
delete this.spec.template.spec.domain.features['smm'];
} }
}, },
@@ -1427,9 +1414,9 @@ export default {
} }
}, },
setTPM({ tpmEnabled = false, tpmPersistentStateEnabled = false } = {}) { setTPM(tpmEnabled) {
if (tpmEnabled) { if (tpmEnabled) {
set(this.spec.template.spec.domain.devices, 'tpm', tpmPersistentStateEnabled ? { persistent: true } : {}); set(this.spec.template.spec.domain.devices, 'tpm', {});
} else { } else {
delete this.spec.template.spec.domain.devices['tpm']; delete this.spec.template.spec.domain.devices['tpm'];
} }
@@ -1542,21 +1529,11 @@ export default {
}, },
efiEnabled(val) { efiEnabled(val) {
this.setBootMethod({ this.setBootMethod({ efi: val, secureBoot: this.secureBoot });
efi: val, secureBoot: this.secureBoot, efiPersistentStateEnabled: this.efiPersistentStateEnabled
});
}, },
secureBoot(val) { secureBoot(val) {
this.setBootMethod({ this.setBootMethod({ efi: this.efiEnabled, secureBoot: val });
efi: this.efiEnabled, secureBoot: val, efiPersistentStateEnabled: this.efiPersistentStateEnabled
});
},
efiPersistentStateEnabled(val) {
this.setBootMethod({
efi: this.efiEnabled, secureBoot: this.secureBoot, efiPersistentStateEnabled: val
});
}, },
cpuPinning(value) { cpuPinning(value) {
@@ -1564,11 +1541,7 @@ export default {
}, },
tpmEnabled(val) { tpmEnabled(val) {
this.setTPM({ tpmEnabled: val, tpmPersistentStateEnabled: this.tpmPersistentStateEnabled }); this.setTPM(val);
},
tpmPersistentStateEnabled(val) {
this.setTPM({ tpmEnabled: this.tpmEnabled, tpmPersistentStateEnabled: val });
}, },
installAgent: { installAgent: {
+2 -1
View File
@@ -67,7 +67,8 @@ export default class HciNamespace extends namespace {
promptRemove(resources = this) { promptRemove(resources = this) {
this.$dispatch('promptModal', { this.$dispatch('promptModal', {
resources, resources,
component: 'ConfirmRelatedToRemoveDialog' warningMessageKey: 'promptRemove.confirmRelatedResource',
component: 'ConfirmRelatedToRemoveDialog'
}); });
} }
+4 -10
View File
@@ -115,7 +115,8 @@ export default class HciNode extends HarvesterResource {
promptRemove(resources = this) { promptRemove(resources = this) {
this.$dispatch('promptModal', { this.$dispatch('promptModal', {
resources, resources,
component: 'ConfirmRelatedToRemoveDialog' warningMessageKey: 'promptRemove.confirmRelatedResource',
component: 'ConfirmRelatedToRemoveDialog'
}); });
} }
@@ -185,16 +186,9 @@ export default class HciNode extends HarvesterResource {
} }
get isKVMDisable() { 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]; const allNotExist = !this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_AMD_CPU] && !this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_INTEL_CPU];
return allNotExist || this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_AMD_CPU] === 'false' || this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_INTEL_CPU] === 'false'; return this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_AMD_CPU] === 'false' || this.metadata?.labels?.[HCI_ANNOTATIONS.KVM_INTEL_CPU] === 'false' || allNotExist;
} }
get stateDisplay() { get stateDisplay() {
@@ -476,7 +470,7 @@ export default class HciNode extends HarvesterResource {
get canDelete() { get canDelete() {
const nodes = this.$rootGetters['harvester/all'](NODE) || []; const nodes = this.$rootGetters['harvester/all'](NODE) || [];
return nodes.length > 1 && super.canDelete; return nodes.length > 1;
} }
get vlanStatuses() { get vlanStatuses() {
@@ -46,37 +46,22 @@ export default class HciPv extends HarvesterResource {
} }
} }
const exportImageAction = { if (!this.isLonghorn || !this.isLonghornV2) {
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 = [ out = [
exportImageAction, {
takeSnapshotAction, 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'),
},
...out ...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 [ return [
@@ -336,31 +321,15 @@ export default class HciPv extends HarvesterResource {
return this.volumeProvider === LONGHORN_DRIVER; return this.volumeProvider === LONGHORN_DRIVER;
} }
get isLonghornV1() {
return this.isLonghorn && !this.isLonghornV2;
}
get isLonghornV2() { get isLonghornV2() {
return this.dataEngine === DATA_ENGINE_V2; 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() { get resourceExternalLink() {
const host = window.location.host; const host = window.location.host;
const { params } = this.currentRoute(); const { params } = this.currentRoute();
const volumeName = this.spec?.volumeName; const volumeName = this.spec?.volumeName;
if (!this.isLonghorn) {
return null;
}
if (!volumeName) { if (!volumeName) {
return null; return null;
} }
@@ -58,18 +58,6 @@ export default class HciStorageClass extends StorageClass {
return key ? this.$rootGetters['i18n/t'](key) : this.provisioner; 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() { get isLonghornV2() {
return this.provisioner === LONGHORN_DRIVER && this.longhornVersion === DATA_ENGINE_V2; return this.provisioner === LONGHORN_DRIVER && this.longhornVersion === DATA_ENGINE_V2;
} }
@@ -81,8 +69,4 @@ export default class HciStorageClass extends StorageClass {
get volumeEncryptionFeatureEnabled() { get volumeEncryptionFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption'); return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
} }
get thirdPartyStorageFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
}
} }
@@ -28,15 +28,17 @@ export default class HciSetting extends HarvesterResource {
} }
const schema = this.$getters['schemaFor'](HCI.UPGRADE); const schema = this.$getters['schemaFor'](HCI.UPGRADE);
const hasUpgradeAccess = !!schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())); const hasUpgradeAccess = !!schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase()));
if (this.id === HCI_SETTING.SERVER_VERSION && hasUpgradeAccess) { if (this.id === HCI_SETTING.SERVER_VERSION && hasUpgradeAccess) {
const latestUpgrade = this.$getters['all'](HCI.UPGRADE).find((upgrade) => upgrade.isLatestUpgrade);
out.unshift({ out.unshift({
action: 'goToAirgapUpgrade', action: 'goToAirgapUpgrade',
enabled: true, enabled: true,
icon: 'icon icon-refresh', icon: 'icon icon-refresh',
label: this.t('harvester.upgradePage.upgrade'), label: this.t('harvester.upgradePage.upgrade'),
disabled: !!latestUpgrade && !latestUpgrade?.isUpgradeSucceeded
}); });
} }
@@ -132,7 +132,7 @@ export default class HciVmBackup extends HarvesterResource {
} }
get sourceSchedule() { get sourceSchedule() {
return this.metadata?.annotations?.[HCI_ANNOTATIONS.SVM_BACKUP_ID]; return this.metadata?.annotations[HCI_ANNOTATIONS.SVM_BACKUP_ID];
} }
get attachVM() { get attachVM() {
@@ -12,7 +12,6 @@ import { HCI } from '../types';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester'; import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
import HarvesterResource from './harvester'; import HarvesterResource from './harvester';
import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map'; import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map';
import { UNIT_SUFFIX } from '../utils/unit';
const { const {
CSI_PROVISIONER_SECRET_NAME, CSI_PROVISIONER_SECRET_NAME,
@@ -50,45 +49,35 @@ export default class HciVmImage extends HarvesterResource {
canCreateVM = false; canCreateVM = false;
} }
const customActions = this.isReady ? [ return [
{ {
action: 'createFromImage', action: 'createFromImage',
enabled: canCreateVM, enabled: canCreateVM,
icon: 'icon icon-circle-plus', icon: 'icon icon-circle-plus',
label: this.t('harvester.action.createVM'), label: this.t('harvester.action.createVM'),
disabled: !this.isReady,
}, },
{ {
action: 'encryptImage', action: 'encryptImage',
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted, enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted,
icon: 'icon icon-lock', icon: 'icon icon-lock',
label: this.t('harvester.action.encryptImage'), label: this.t('harvester.action.encryptImage'),
disabled: !this.isReady,
}, },
{ {
action: 'decryptImage', action: 'decryptImage',
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted, enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted,
icon: 'icon icon-unlock', icon: 'icon icon-unlock',
label: this.t('harvester.action.decryptImage'), label: this.t('harvester.action.decryptImage'),
disabled: !this.isReady,
}, },
{ {
action: 'imageDownload', action: 'download',
enabled: this.links?.download, enabled: this.links?.download,
icon: 'icon icon-download', icon: 'icon icon-download',
label: this.t('asyncButton.download.action'), label: this.t('asyncButton.download.action'),
} },
] : []; ...out
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
]; ];
} }
@@ -210,10 +199,6 @@ export default class HciVmImage extends HarvesterResource {
return `${ this.metadata.namespace }/${ this.spec.displayName }`; return `${ this.metadata.namespace }/${ this.spec.displayName }`;
} }
get imageStorageClass() {
return this?.metadata?.annotations?.[HCI_ANNOTATIONS.STORAGE_CLASS] || '';
}
get imageMessage() { get imageMessage() {
if (this.uploadError) { if (this.uploadError) {
return ucFirst(this.uploadError); return ucFirst(this.uploadError);
@@ -254,8 +239,8 @@ export default class HciVmImage extends HarvesterResource {
return formatSi(size, { return formatSi(size, {
increment: 1024, increment: 1024,
maxPrecision: 2, maxPrecision: 2,
suffix: UNIT_SUFFIX, suffix: 'B',
firstSuffix: UNIT_SUFFIX, firstSuffix: 'B',
}); });
} }
@@ -269,8 +254,8 @@ export default class HciVmImage extends HarvesterResource {
return formatSi(virtualSize, { return formatSi(virtualSize, {
increment: 1024, increment: 1024,
maxPrecision: 2, maxPrecision: 2,
suffix: UNIT_SUFFIX, suffix: 'B',
firstSuffix: UNIT_SUFFIX, firstSuffix: 'B',
}); });
} }
@@ -305,7 +290,7 @@ export default class HciVmImage extends HarvesterResource {
} }
get uploadImage() { get uploadImage() {
return async(file, opt = {}) => { return async(file) => {
const formData = new FormData(); const formData = new FormData();
formData.append('chunk', file); formData.append('chunk', file);
@@ -313,23 +298,22 @@ export default class HciVmImage extends HarvesterResource {
try { try {
this.$ctx.commit('harvester-common/uploadStart', this.metadata.name, { root: true }); this.$ctx.commit('harvester-common/uploadStart', this.metadata.name, { root: true });
const result = await this.doAction('upload', formData, { await this.doAction('upload', formData, {
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
'File-Size': file.size, 'File-Size': file.size,
}, },
params: { size: file.size }, params: { size: file.size },
signal: opt.signal,
}); });
return result;
} catch (err) { } catch (err) {
this.$ctx.commit('harvester-common/uploadError', { name: this.name, message: err.message }, { root: true }); 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 }); this.$ctx.commit('harvester-common/uploadEnd', this.metadata.name, { root: true });
throw err;
} finally { return Promise.reject(err);
this.$ctx.commit('harvester-common/uploadEnd', this.metadata.name, { root: true });
} }
this.$ctx.commit('harvester-common/uploadEnd', this.metadata.name, { root: true });
}; };
} }
@@ -401,23 +385,7 @@ export default class HciVmImage extends HarvesterResource {
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption'); return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
} }
get thirdPartyStorageFeatureEnabled() { download() {
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; window.location.href = this.links.download;
} }
} }
@@ -210,10 +210,6 @@ export default class HciVmTemplateVersion extends HarvesterResource {
await templateResource.save(); await templateResource.save();
} }
get cpuPinningFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('cpuPinning');
}
get defaultVersion() { get defaultVersion() {
const templates = this.$rootGetters['harvester/all'](HCI.VM_TEMPLATE); const templates = this.$rootGetters['harvester/all'](HCI.VM_TEMPLATE);
const template = templates.find((T) => this.templateId === T.id); const template = templates.find((T) => this.templateId === T.id);
@@ -276,12 +272,4 @@ export default class HciVmTemplateVersion extends HarvesterResource {
this.spec.vm.spec.template.metadata['labels'] = { ...wasIgnored, ...val }; 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,8 +1,5 @@
import SteveModel from '@shell/plugins/steve/steve-class'; import SteveModel from '@shell/plugins/steve/steve-class';
import { HCI } from '@shell/config/labels-annotations'; import { HCI } from '@shell/config/labels-annotations';
import { NETWORK_TYPE } from '../config/types';
const { UNTAGGED } = NETWORK_TYPE;
export default class NetworkAttachmentDef extends SteveModel { export default class NetworkAttachmentDef extends SteveModel {
applyDefaults() { applyDefaults() {
@@ -45,7 +42,7 @@ export default class NetworkAttachmentDef extends SteveModel {
} }
get vlanId() { get vlanId() {
return this.vlanType === UNTAGGED ? 'N/A' : this.parseConfig.vlan; return this.vlanType === 'UntaggedNetwork' ? 'N/A' : this.parseConfig.vlan;
} }
get customValidationRules() { get customValidationRules() {
@@ -68,7 +65,7 @@ export default class NetworkAttachmentDef extends SteveModel {
const route = annotations[HCI.NETWORK_ROUTE]; const route = annotations[HCI.NETWORK_ROUTE];
let config = {}; let config = {};
if (this.vlanType === UNTAGGED) { if (this.vlanType === 'UntaggedNetwork') {
return 'N/A'; return 'N/A';
} }
@@ -102,13 +102,11 @@ export default class VirtVm extends HarvesterResource {
return [ return [
{ {
action: 'stopVM', action: 'stopVM',
altAction: 'altStopVM', enabled: !!this.actions?.stop,
enabled: !!this.actions?.stop, icon: 'icon icon-close',
icon: 'icon icon-close', label: this.t('harvester.action.stop'),
label: this.t('harvester.action.stop'), bulkable: true
bulkable: true,
bulkAction: 'stopVM',
}, },
{ {
action: 'forceStop', action: 'forceStop',
@@ -118,11 +116,10 @@ export default class VirtVm extends HarvesterResource {
bulkable: true bulkable: true
}, },
{ {
action: 'pauseVM', action: 'pauseVM',
altAction: 'altPauseVM', enabled: !!this.actions?.pause,
enabled: !!this.actions?.pause, icon: 'icon icon-pause',
icon: 'icon icon-pause', label: this.t('harvester.action.pause')
label: this.t('harvester.action.pause')
}, },
{ {
action: 'unpauseVM', action: 'unpauseVM',
@@ -158,7 +155,7 @@ export default class VirtVm extends HarvesterResource {
}, },
{ {
action: 'takeVMSnapshot', action: 'takeVMSnapshot',
enabled: (!!this.actions?.snapshot || !!this.action?.backup) && !this.longhornV2Volumes.length, enabled: !!this.actions?.backup && !this.longhornV2Volumes.length,
icon: 'icon icon-snapshot', icon: 'icon icon-snapshot',
label: this.t('harvester.action.vmSnapshot') label: this.t('harvester.action.vmSnapshot')
}, },
@@ -405,16 +402,7 @@ export default class VirtVm extends HarvesterResource {
return node?.id; return node?.id;
} }
pauseVM(resources = this) { pauseVM() {
this.$dispatch('promptModal', {
resources,
action: 'pause',
warningMessageKey: 'dialog.confirmExecution.pause.message',
component: 'ConfirmExecutionDialog'
});
}
altPauseVM() {
this.doActionGrowl('pause', {}); this.doActionGrowl('pause', {});
} }
@@ -429,16 +417,7 @@ export default class VirtVm extends HarvesterResource {
this.doActionGrowl('unpause', {}); this.doActionGrowl('unpause', {});
} }
stopVM(resources = this) { stopVM() {
this.$dispatch('promptModal', {
resources,
action: 'stop',
warningMessageKey: 'dialog.confirmExecution.stop.message',
component: 'ConfirmExecutionDialog'
});
}
altStopVM() {
this.doActionGrowl('stop', {}); this.doActionGrowl('stop', {});
} }
@@ -640,10 +619,6 @@ export default class VirtVm extends HarvesterResource {
} }
get encryptedVolumeType() { get encryptedVolumeType() {
if (!this.volumes || this.volumes.length === 0) {
return 'none';
}
if (this.volumes.every((vol) => vol.isEncrypted)) { if (this.volumes.every((vol) => vol.isEncrypted)) {
return 'all'; return 'all';
} else if (this.volumes.some((vol) => vol.isEncrypted)) { } else if (this.volumes.some((vol) => vol.isEncrypted)) {
@@ -700,25 +675,8 @@ export default class VirtVm extends HarvesterResource {
return null; return null;
} }
get isPending() { get isBeingStopped() {
if (this && if (this && !this.isVMExpectedRunning && this.isVMCreated && this.vmi?.status?.phase !== VMIPhase.Succeeded) {
!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 }; return { status: STOPPING };
} }
@@ -753,7 +711,7 @@ export default class VirtVm extends HarvesterResource {
} }
get isUnschedulable() { get isUnschedulable() {
if (this.isStopping || this.isStarting) { if (this.isBeingStopped || this.isStarting) {
const condition = this.status?.conditions?.find((c) => c.reason === UNSCHEDULABLE); const condition = this.status?.conditions?.find((c) => c.reason === UNSCHEDULABLE);
if (!!condition) { if (!!condition) {
@@ -869,8 +827,7 @@ export default class VirtVm extends HarvesterResource {
this.isUnschedulable?.status || this.isUnschedulable?.status ||
this.isPaused?.status || this.isPaused?.status ||
this.isVMError?.status || this.isVMError?.status ||
this.isPending?.status || this.isBeingStopped?.status ||
this.isStopping?.status ||
this.isOff?.status || this.isOff?.status ||
this.isError?.status || this.isError?.status ||
this.isRunning?.status || this.isRunning?.status ||
@@ -1193,18 +1150,6 @@ export default class VirtVm extends HarvesterResource {
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption'); 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) { setInstanceLabels(val) {
if ( !this.spec?.template?.metadata?.labels ) { if ( !this.spec?.template?.metadata?.labels ) {
set(this, 'spec.template.metadata.labels', {}); set(this, 'spec.template.metadata.labels', {});
+2 -2
View File
@@ -1,13 +1,13 @@
{ {
"name": "harvester", "name": "harvester",
"description": "Rancher UI Extension for Harvester", "description": "Rancher UI Extension for Harvester",
"version": "1.5.1", "version": "1.0.6",
"private": false, "private": false,
"rancher": { "rancher": {
"annotations": { "annotations": {
"catalog.cattle.io/display-name": "Harvester", "catalog.cattle.io/display-name": "Harvester",
"catalog.cattle.io/kube-version": ">= 1.16.0-0", "catalog.cattle.io/kube-version": ">= 1.16.0-0",
"catalog.cattle.io/rancher-version": ">= 2.11.0-0", "catalog.cattle.io/rancher-version": ">= 2.10.1-0",
"catalog.cattle.io/ui-extensions-version": ">= 3.0.0 < 4.0.0" "catalog.cattle.io/ui-extensions-version": ">= 3.0.0 < 4.0.0"
} }
}, },
@@ -7,16 +7,13 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
import { exceptionToErrorsArray } from '@shell/utils/error'; import { exceptionToErrorsArray } from '@shell/utils/error';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import UpgradeInfo from '../../../../components/UpgradeInfo'; import UpgradeInfo from '../../../../components/UpgradeInfo';
import { HCI } from '../../../../types'; import { HCI } from '../../../../types';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../../config/harvester'; 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 = { const IMAGE_METHOD = {
NEW: 'new', NEW: 'new',
EXIST: 'exist', EXIST: 'exist'
DELETE: 'delete'
}; };
const DOWNLOAD = 'download'; const DOWNLOAD = 'download';
@@ -25,7 +22,7 @@ const UPLOAD = 'upload';
export default { export default {
name: 'HarvesterAirgapUpgrade', name: 'HarvesterAirgapUpgrade',
components: { components: {
Checkbox, CruResource, LabeledSelect, LabeledInput, RadioGroup, UpgradeInfo, ImagePercentageBar, Banner Checkbox, CruResource, LabeledSelect, LabeledInput, RadioGroup, UpgradeInfo
}, },
inheritAttrs: false, inheritAttrs: false,
@@ -42,32 +39,36 @@ export default {
spec: { image: '' } spec: { image: '' }
}); });
await this.initImageValue(); const imageValue = await this.$store.dispatch('harvester/create', {
this.value = value; type: HCI.IMAGE,
}, metadata: {
name: '',
namespace: 'harvester-system',
generateName: 'image-',
annotations: {}
},
spec: {
sourceType: UPLOAD,
displayName: '',
checksum: ''
},
});
beforeUnmount() { this.value = value;
if (this.uploadController) { this.imageValue = imageValue;
this.uploadController.abort();
}
}, },
data() { data() {
return { return {
value: null, value: null,
file: {}, file: {},
uploadImageId: '', imageId: '',
imageId: '', imageSource: IMAGE_METHOD.NEW,
deleteImageId: '', sourceType: UPLOAD,
imageSource: IMAGE_METHOD.NEW, imageValue: null,
sourceType: UPLOAD, errors: [],
uploadController: null, enableLogging: true,
uploadResult: null, IMAGE_METHOD
imageValue: null,
enableLogging: true,
IMAGE_METHOD,
skipSingleReplicaDetachedVol: false,
errors: [],
}; };
}, },
@@ -76,45 +77,22 @@ export default {
return `${ HARVESTER_PRODUCT }-c-cluster-resource`; return `${ HARVESTER_PRODUCT }-c-cluster-resource`;
}, },
skipSingleReplicaDetachedVolFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
},
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() { osImageOptions() {
return this.allOSImages.map((I) => { return this.$store.getters['harvester/all'](HCI.IMAGE)
return { .filter((I) => I.isOSImage)
label: I.spec.displayName, .map((I) => {
value: I.id, return {
disabled: !I.isReady label: I.spec.displayName,
}; value: I.id,
}); disabled: !I.isReady
};
});
}, },
createNewImage() { uploadImage() {
return this.imageSource === IMAGE_METHOD.NEW; return this.imageSource === IMAGE_METHOD.NEW;
}, },
selectExistImage() {
return this.imageSource === IMAGE_METHOD.EXIST;
},
deleteExistImage() {
return this.imageSource === IMAGE_METHOD.DELETE;
},
fileName() { fileName() {
return this.file?.name || ''; return this.file?.name || '';
}, },
@@ -122,89 +100,21 @@ export default {
canEnableLogging() { canEnableLogging() {
return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG); 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: { methods: {
done() { done() {
if (this.uploadController) {
this.uploadController.abort();
}
this.$router.push({ this.$router.push({
name: this.doneRoute, name: this.doneRoute,
params: { resource: HCI.SETTING, product: 'harvester' } 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) { async save(buttonCb) {
let res = null; let res = null;
this.file = {};
this.errors = []; 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') })); this.errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('generic.name') }));
buttonCb(false); buttonCb(false);
@@ -215,11 +125,26 @@ export default {
if (this.imageSource === IMAGE_METHOD.NEW) { if (this.imageSource === IMAGE_METHOD.NEW) {
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True'; this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
if (this.sourceType === UPLOAD && this.uploadImageId !== '') { if (this.sourceType === UPLOAD) {
this.value.spec.image = this.uploadImageId; 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);
} else if (this.sourceType === DOWNLOAD) { } else if (this.sourceType === DOWNLOAD) {
this.imageValue.spec.sourceType = DOWNLOAD; this.imageValue.spec.sourceType = DOWNLOAD;
if (!this.imageValue.spec.url) { if (!this.imageValue.spec.url) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.imageUrl')); this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.imageUrl'));
buttonCb(false); buttonCb(false);
@@ -228,12 +153,12 @@ export default {
} }
res = await this.imageValue.save(); res = await this.imageValue.save();
this.value.spec.image = res.id;
} }
this.value.spec.image = res.id;
} else if (this.imageSource === IMAGE_METHOD.EXIST) { } else if (this.imageSource === IMAGE_METHOD.EXIST) {
if (!this.imageId) { if (!this.imageId) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.chooseFile')); this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.chooseFile'));
buttonCb(false);
return; return;
} }
@@ -244,87 +169,18 @@ export default {
if (this.canEnableLogging) { if (this.canEnableLogging) {
this.value.spec.logEnabled = this.enableLogging; 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(); await this.value.save();
this.done(); this.done();
buttonCb(true); buttonCb(true);
} catch (e) { } catch (e) {
this.errors = [e?.message] || exceptionToErrorsArray(e); this.errors = exceptionToErrorsArray(e);
buttonCb(false); buttonCb(false);
} }
}, },
async uploadFile(file) { handleFileUpload() {
const fileName = file.name; this.file = this.$refs.file.files[0];
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() { selectFile() {
@@ -334,27 +190,21 @@ export default {
}, },
watch: { watch: {
imageSource(neu) {
if (neu !== IMAGE_METHOD.DELETE) {
this.deleteImageId = '';
}
},
'imageValue.spec.url': { 'imageValue.spec.url': {
handler(neu) { handler(neu) {
const suffixName = neu?.split('/')?.pop(); const suffixName = neu?.split('/')?.pop();
const splitName = suffixName?.split('.') || []; const splitName = suffixName?.split('.') || [];
const fileSuffix = splitName?.pop()?.toLowerCase(); const fileSuffix = splitName?.pop()?.toLowerCase();
if (splitName.length > 1 && fileSuffix === 'iso' && suffixName !== this.imageValue.spec.displayName) { if (splitName.length > 1 && fileSuffix === 'iso' && !this.imageValue.spec.displayName) {
this.imageValue.spec.displayName = suffixName; this.imageValue.spec.displayName = suffixName;
} }
}, },
deep: true deep: true
}, },
file(neu) { file(neu) {
// update name input if select new image if (!this.imageValue.spec.displayName && neu.name) {
if (neu.name && neu.name !== this.imageValue.spec.displayName) {
this.imageValue.spec.displayName = neu.name; this.imageValue.spec.displayName = neu.name;
} }
} }
@@ -377,11 +227,9 @@ export default {
:errors="errors" :errors="errors"
:can-yaml="false" :can-yaml="false"
finish-button-mode="upgrade" finish-button-mode="upgrade"
:validation-passed="enableUpgrade"
:cancel-event="true" :cancel-event="true"
@finish="save" @finish="save"
@cancel="done" @cancel="done"
@error="e=>errors = e"
> >
<RadioGroup <RadioGroup
v-model:value="imageSource" v-model:value="imageSource"
@@ -390,57 +238,18 @@ export default {
:options="[ :options="[
IMAGE_METHOD.NEW, IMAGE_METHOD.NEW,
IMAGE_METHOD.EXIST, IMAGE_METHOD.EXIST,
IMAGE_METHOD.DELETE,
]" ]"
:labels="[ :labels="[
t('harvester.upgradePage.uploadNew'), t('harvester.upgradePage.uploadNew'),
t('harvester.upgradePage.selectExisting'), t('harvester.upgradePage.selectExisting'),
t('harvester.upgradePage.deleteExisting'),
]" ]"
/> />
<UpgradeInfo v-if="createNewImage || selectExistImage" /> <UpgradeInfo />
<Banner <div v-if="uploadImage">
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 <LabeledInput
v-model:value.trim="imageValue.spec.displayName" v-model.trim="imageValue.spec.displayName"
class="mb-20" class="mb-20"
label-key="harvester.fields.name" label-key="harvester.fields.name"
required required
@@ -452,6 +261,14 @@ export default {
label-key="harvester.setting.upgrade.checksum" 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 <RadioGroup
v-model:value="sourceType" v-model:value="sourceType"
class="mb-20 image-group" class="mb-20 image-group"
@@ -468,7 +285,7 @@ export default {
<LabeledInput <LabeledInput
v-if="sourceType === 'download'" v-if="sourceType === 'download'"
v-model:value.trim="imageValue.spec.url" v-model.trim="imageValue.spec.url"
class="labeled-input--tooltip" class="labeled-input--tooltip"
required required
label-key="harvester.image.url" label-key="harvester.image.url"
@@ -481,7 +298,6 @@ export default {
<button <button
type="button" type="button"
class="btn role-primary" class="btn role-primary"
:disabled="disableUploadButton"
@click="selectFile" @click="selectFile"
> >
{{ t('harvester.image.uploadFile') }} {{ t('harvester.image.uploadFile') }}
@@ -502,56 +318,28 @@ export default {
{{ fileName ? fileName : t('harvester.generic.noFileChosen') }} {{ fileName ? fileName : t('harvester.generic.noFileChosen') }}
</span> </span>
</div> </div>
<ImagePercentageBar
v-if="showProgressBar"
class="mt-20"
:value="uploadProgress"
/>
</div> </div>
<LabeledSelect <LabeledSelect
v-if="selectExistImage" v-else
v-model:value="imageId" v-model:value="imageId"
:options="osImageOptions" :options="osImageOptions"
required required
class="mb-20" class="mb-20"
label-key="harvester.fields.image" 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> </CruResource>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
#air-gap { #air-gap {
padding: 20px;
:deep() .image-group .radio-group { :deep() .image-group .radio-group {
display: flex; display: flex;
.radio-container { .radio-container {
margin-right: 30px; margin-right: 30px;
} }
} }
.parent {
grid-template-columns:auto 40px;
}
.chooseFile { .chooseFile {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -16,12 +16,14 @@ export default {
return []; return [];
} }
}, },
names: { names: {
type: Array, type: Array,
default: () => { default: () => {
return []; return [];
} }
}, },
type: { type: {
type: String, type: String,
required: true required: true
@@ -67,6 +69,12 @@ export default {
return out; return out;
}, },
plusMore() {
const remaining = this.toRemove.length - this.names.length;
return this.t('promptRemove.andOthers', { count: remaining });
},
}, },
watch: { watch: {
@@ -135,7 +143,7 @@ export default {
<div> <div>
<div class="mt-10"> <div class="mt-10">
{{ t('promptRemove.attemptingToRemove', {type}) }} {{ t('promptRemove.attemptingToRemove', {type}) }}
<span v-clean-html="resourceNames(names, t)"></span> <span v-clean-html="resourceNames(names, plusMore, t)"></span>
<div class="mt-10"> <div class="mt-10">
{{ t('harvester.virtualMachine.promptRemove.title') }} {{ t('harvester.virtualMachine.promptRemove.title') }}
+7 -96
View File
@@ -5,33 +5,16 @@ import { featureEnabled, getVersion } from '../utils/feature-flags';
const state = function() { const state = function() {
return { return {
// support bundle latestBundleId: '',
latestBundleId: '', bundlePending: false,
bundlePending: false, showBundleModal: false,
showBundleModal: false, bundlePercentage: 0,
bundlePercentage: 0, uploadingImages: [],
uploadingImages: [], uploadingImageError: {},
uploadingImageError: {},
// download cdi image
downloadImageId: '',
downloadImageInProgress: false,
isDownloadImageCancel: false,
}; };
}; };
const mutations = { const mutations = {
setDownloadImageId(state, id) {
state.downloadImageId = id;
},
setDownloadImageCancel(state, value) {
state.isDownloadImageCancel = value;
},
setDownloadImageInProgress(state, value) {
state.downloadImageInProgress = value;
},
setLatestBundleId(state, bundleId) { setLatestBundleId(state, bundleId) {
state.latestBundleId = bundleId; state.latestBundleId = bundleId;
}, },
@@ -68,14 +51,6 @@ const getters = {
return state.latestBundleId; return state.latestBundleId;
}, },
isDownloadImageCancel(state) {
return state.isDownloadImageCancel;
},
isDownloadImageInProgress(state) {
return state.downloadImageInProgress;
},
isBundlePending(state) { isBundlePending(state) {
return state.bundlePending; return state.bundlePending;
}, },
@@ -123,70 +98,6 @@ const getters = {
}; };
const actions = { 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({ async bundleProgress({
state, dispatch, commit, rootGetters state, dispatch, commit, rootGetters
}) { }) {
@@ -206,7 +117,7 @@ const actions = {
const timer = setInterval(async() => { const timer = setInterval(async() => {
count = count + 1; count = count + 1;
if (count % 3 === 0) { if (count % 3 === 0) {
// ws maybe disconnect // ws mayby disconnect
bundleCrd = await dispatch( bundleCrd = await dispatch(
'harvester/find', 'harvester/find',
{ {
+52 -53
View File
@@ -1,57 +1,56 @@
export const HCI = { export const HCI = {
VM: 'kubevirt.io.virtualmachine', VM: 'kubevirt.io.virtualmachine',
VMI: 'kubevirt.io.virtualmachineinstance', VMI: 'kubevirt.io.virtualmachineinstance',
VMIM: 'kubevirt.io.virtualmachineinstancemigration', VMIM: 'kubevirt.io.virtualmachineinstancemigration',
VM_TEMPLATE: 'harvesterhci.io.virtualmachinetemplate', VM_TEMPLATE: 'harvesterhci.io.virtualmachinetemplate',
VM_VERSION: 'harvesterhci.io.virtualmachinetemplateversion', VM_VERSION: 'harvesterhci.io.virtualmachinetemplateversion',
IMAGE: 'harvesterhci.io.virtualmachineimage', IMAGE: 'harvesterhci.io.virtualmachineimage',
SSH: 'harvesterhci.io.keypair', SSH: 'harvesterhci.io.keypair',
VOLUME: 'harvesterhci.io.volume', VOLUME: 'harvesterhci.io.volume',
USER: 'harvesterhci.io.user', USER: 'harvesterhci.io.user',
SETTING: 'harvesterhci.io.setting', SETTING: 'harvesterhci.io.setting',
UPGRADE: 'harvesterhci.io.upgrade', UPGRADE: 'harvesterhci.io.upgrade',
UPGRADE_LOG: 'harvesterhci.io.upgradelog', UPGRADE_LOG: 'harvesterhci.io.upgradelog',
SCHEDULE_VM_BACKUP: 'harvesterhci.io.schedulevmbackup', SCHEDULE_VM_BACKUP: 'harvesterhci.io.schedulevmbackup',
BACKUP: 'harvesterhci.io.virtualmachinebackup', BACKUP: 'harvesterhci.io.virtualmachinebackup',
RESTORE: 'harvesterhci.io.virtualmachinerestore', RESTORE: 'harvesterhci.io.virtualmachinerestore',
NODE_NETWORK: 'network.harvesterhci.io.nodenetwork', NODE_NETWORK: 'network.harvesterhci.io.nodenetwork',
CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork', CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork',
VM_IMAGE_DOWNLOADER: 'harvesterhci.io.virtualmachineimagedownloader', SUPPORT_BUNDLE: 'harvesterhci.io.supportbundle',
SUPPORT_BUNDLE: 'harvesterhci.io.supportbundle', NETWORK_ATTACHMENT: 'harvesterhci.io.networkattachmentdefinition',
NETWORK_ATTACHMENT: 'harvesterhci.io.networkattachmentdefinition', CLUSTER: 'harvesterhci.io.management.cluster',
CLUSTER: 'harvesterhci.io.management.cluster', DASHBOARD: 'harvesterhci.io.dashboard',
DASHBOARD: 'harvesterhci.io.dashboard', BLOCK_DEVICE: 'harvesterhci.io.blockdevice',
BLOCK_DEVICE: 'harvesterhci.io.blockdevice', CLOUD_TEMPLATE: 'harvesterhci.io.cloudtemplate',
CLOUD_TEMPLATE: 'harvesterhci.io.cloudtemplate', HOST: 'harvesterhci.io.host',
HOST: 'harvesterhci.io.host', VERSION: 'harvesterhci.io.version',
VERSION: 'harvesterhci.io.version', SNAPSHOT: 'harvesterhci.io.volumesnapshot',
SNAPSHOT: 'harvesterhci.io.volumesnapshot', VM_SNAPSHOT: 'harvesterhci.io.vmsnapshot',
VM_SNAPSHOT: 'harvesterhci.io.vmsnapshot', ALERTMANAGERCONFIG: 'harvesterhci.io.monitoring.alertmanagerconfig',
ALERTMANAGERCONFIG: 'harvesterhci.io.monitoring.alertmanagerconfig', CLUSTER_FLOW: 'harvesterhci.io.logging.clusterflow',
CLUSTER_FLOW: 'harvesterhci.io.logging.clusterflow', CLUSTER_OUTPUT: 'harvesterhci.io.logging.clusteroutput',
CLUSTER_OUTPUT: 'harvesterhci.io.logging.clusteroutput', FLOW: 'harvesterhci.io.logging.flow',
FLOW: 'harvesterhci.io.logging.flow', OUTPUT: 'harvesterhci.io.logging.output',
OUTPUT: 'harvesterhci.io.logging.output', STORAGE: 'harvesterhci.io.storage',
STORAGE: 'harvesterhci.io.storage', RESOURCE_QUOTA: 'harvesterhci.io.resourcequota',
RESOURCE_QUOTA: 'harvesterhci.io.resourcequota', KSTUNED: 'node.harvesterhci.io.ksmtuned',
KSTUNED: 'node.harvesterhci.io.ksmtuned', PCI_DEVICE: 'devices.harvesterhci.io.pcidevice',
PCI_DEVICE: 'devices.harvesterhci.io.pcidevice', PCI_CLAIM: 'devices.harvesterhci.io.pcideviceclaim',
PCI_CLAIM: 'devices.harvesterhci.io.pcideviceclaim', SR_IOV: 'devices.harvesterhci.io.sriovnetworkdevice',
SR_IOV: 'devices.harvesterhci.io.sriovnetworkdevice', VGPU_DEVICE: 'devices.harvesterhci.io.vgpudevice',
VGPU_DEVICE: 'devices.harvesterhci.io.vgpudevice', SR_IOVGPU_DEVICE: 'devices.harvesterhci.io.sriovgpudevice',
SR_IOVGPU_DEVICE: 'devices.harvesterhci.io.sriovgpudevice', USB_DEVICE: 'devices.harvesterhci.io.usbdevice',
USB_DEVICE: 'devices.harvesterhci.io.usbdevice', USB_CLAIM: 'devices.harvesterhci.io.usbdeviceclaim',
USB_CLAIM: 'devices.harvesterhci.io.usbdeviceclaim', VLAN_CONFIG: 'network.harvesterhci.io.vlanconfig',
VLAN_CONFIG: 'network.harvesterhci.io.vlanconfig', VLAN_STATUS: 'network.harvesterhci.io.vlanstatus',
VLAN_STATUS: 'network.harvesterhci.io.vlanstatus', ADD_ONS: 'harvesterhci.io.addon',
ADD_ONS: 'harvesterhci.io.addon', LINK_MONITOR: 'network.harvesterhci.io.linkmonitor',
LINK_MONITOR: 'network.harvesterhci.io.linkmonitor', SECRET: 'harvesterhci.io.secret',
SECRET: 'harvesterhci.io.secret', INVENTORY: 'metal.harvesterhci.io.inventory',
INVENTORY: 'metal.harvesterhci.io.inventory', LB: 'loadbalancer.harvesterhci.io.loadbalancer',
LB: 'loadbalancer.harvesterhci.io.loadbalancer', IP_POOL: 'loadbalancer.harvesterhci.io.ippool',
IP_POOL: 'loadbalancer.harvesterhci.io.ippool', HARVESTER_CONFIG: 'rke-machine-config.cattle.io.harvesterconfig',
HARVESTER_CONFIG: 'rke-machine-config.cattle.io.harvesterconfig', LVM_VOLUME_GROUP: 'harvesterhci.io.lvmvolumegroup'
LVM_VOLUME_GROUP: 'harvesterhci.io.lvmvolumegroup'
}; };
export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot'; export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot';
-10
View File
@@ -35,17 +35,7 @@ function latestMinorVersion(v) {
} }
export const featureEnabled = (featureKey, serverVersion) => { export const featureEnabled = (featureKey, serverVersion) => {
const minSupportedVersion = '1.3.0';
const version = getVersion(serverVersion); const version = getVersion(serverVersion);
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 }`);
return false;
}
let releasedFeatures = RELEASE_FEATURES[version]; let releasedFeatures = RELEASE_FEATURES[version];
if (!releasedFeatures) { if (!releasedFeatures) {
-3
View File
@@ -1,3 +0,0 @@
export const UNIT_SUFFIX = 'i';
export const GIBIBYTE = 'Gi';
export const MEBIBYTE = 'Mi';
+1 -2
View File
@@ -2,7 +2,6 @@ import { PVC } from '@shell/config/types';
import { isValidMac, isValidDNSLabelName } from '@pkg/utils/regular'; import { isValidMac, isValidDNSLabelName } from '@pkg/utils/regular';
import { SOURCE_TYPE } from '@pkg/config/harvester-map'; import { SOURCE_TYPE } from '@pkg/config/harvester-map';
import { parseVolumeClaimTemplates } from '@pkg/utils/vm'; import { parseVolumeClaimTemplates } from '@pkg/utils/vm';
import { GIBIBYTE } from '../utils/unit';
const maxNameLength = 63; const maxNameLength = 63;
@@ -89,7 +88,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)) { 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 ${ GIBIBYTE }` }); const message = getters['i18n/t']('harvester.validation.generic.maximumSize', { max: '999999999 GiB' });
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message })); errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
} }
+1 -3
View File
@@ -1,5 +1,3 @@
import { GIBIBYTE } from '../utils/unit';
export function volumeSize(size, getters, errors, validatorArgs, displayKey, value) { export function volumeSize(size, getters, errors, validatorArgs, displayKey, value) {
if (!size) { if (!size) {
const key = getters['i18n/t']('harvester.volume.size'); const key = getters['i18n/t']('harvester.volume.size');
@@ -8,7 +6,7 @@ export function volumeSize(size, getters, errors, validatorArgs, displayKey, val
} }
if (size && !/^([0-9][0-9]{0,8})[a-zA-Z]+$/.test(size)) { if (size && !/^([0-9][0-9]{0,8})[a-zA-Z]+$/.test(size)) {
const message = getters['i18n/t']('harvester.validation.generic.maximumSize', { max: `999999999 ${ GIBIBYTE }` }); const message = getters['i18n/t']('harvester.validation.generic.maximumSize', { max: '999999999 GiB' });
errors.push(message); errors.push(message);
} }
+1400 -1188
View File
File diff suppressed because it is too large Load Diff