Compare commits

..
66 Commits
Author SHA1 Message Date
Andy Lee 40d16d0bf3 chore: bump to v1.6.1 (#561)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-16 12:35:54 +08:00
renovate[bot] 3e013bc4fc deps: update patch digest dependencies (#556)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-12 13:39:59 +00:00
freeze d31979a91f chore: upgrade to v1.6.1-rc2 (#550)
Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
2025-10-08 21:27:29 +08:00
mergify[bot]andAndy Lee 3ef5a14567 fix: allow to edit as yaml if has empty CPU or memory (#547) (#549)
* fix: allow edit as yaml in create VM page if empty CPU or memory



* refactor: remove getCPUMemoryValidation



---------


(cherry picked from commit 5c2a23924d)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-10-08 16:24:50 +08:00
renovate[bot] 1e0e2ea867 deps: update dependency @types/node to v20.19.19 (#545)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-05 13:32:14 +00:00
mergify[bot]andAndy Lee 16e5fd3177 fix: set maxSockets : 1 if not enabled cpuMemoryhotplug (#539) (#540)
(cherry picked from commit 4bb67153ce)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-10-03 09:43:00 +08:00
Andy Lee 20696f6801 chore: upgrade to v1.6.1-rc1 (#538) 2025-10-01 15:15:56 +08:00
renovate[bot] e5b487ebcb deps: update dependency @types/node to v20.19.17 (#533)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-21 13:46:49 +00:00
renovate[bot] f058698fb5 deps: update dependency @types/node to v20.19.14 (#525)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-14 16:29:10 +00:00
mergify[bot]andfreeze 208496d6f7 chore: modify the check-version script (#490) (#521)
- to pass the dev build


(cherry picked from commit 18be022a9f)

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
Co-authored-by: freeze <1615081+Vicente-Cheng@users.noreply.github.com>
2025-09-12 14:45:05 +08:00
renovate[bot] 967966e852 deps: update dependency @types/node to v20.19.13 (#517)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-07 13:47:57 +00:00
mergify[bot]andAndy Lee f19809da04 refactor: disable the setting detail drawer (#507) (#509)
(cherry picked from commit f932afee68)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-09-02 23:06:15 +08:00
renovate[bot] bc3d9389f2 deps: update minor dependencies (#502)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 15:01:54 +08:00
Andy Lee 6fbdf5244e ci: update renovate.json to define PR title and commit (#508)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-02 14:50:06 +08:00
renovate[bot] 7b21f6bb2e chore(deps): update patch digest dependencies (#501)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-31 17:20:55 +00:00
Andy Lee 0f6faed2dc chore: bump version to v1.6.1-dev (#488)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-28 14:49:19 +08:00
mergify[bot]andAndy Lee 021de41e68 deps: upgrade @rancher/shell to 3.0.5-rc.8 (#469) (#486)
(cherry picked from commit 18599fc94c)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-27 16:26:42 +08:00
Andy Lee 65f4405d29 ci: add PR label workflow (backport multiple PRs) (#485)
* ci: add PR title lint (#458)

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

* ci: only run PR lint in PR stage (#471)

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

* ci: auto add  PR label workflow and script (#466)

* ci: add auto PR label workflow and script

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

* fix: bug fix prefix

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

* refactor: update label mapping

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

* refactor: read commitlint prefix from commitlint.config.js

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

* refactor: rename to scripts/extract-release-label.mjs

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

---------

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

* ci: add .github/release.yml (#468)

* ci: add release.yml

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

* refactor: update release.yml

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

---------

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

* ci: correct set PR label script name (#476)

* fix: set label script name

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

* ci: rerun PR label after PR edited

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

---------

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

* ci: setup env using actions/setup (#478)

* ci: setup env using actions/setup

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

* ci: rerun lint if PR force push

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

---------

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-27 16:20:07 +08:00
Andy Lee 4bae62d046 chore: add v1.6.1 feature flag (#484)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-27 15:56:45 +08:00
mergify[bot]andAndy Lee 4cc60e22a8 fix: reboot button not work in VNC console (#475) (#479)
* fix: reboot button in VNC console



* style: hide the arrow in VNC console



---------


(cherry picked from commit 905db4b12c)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-27 15:37:00 +08:00
mergify[bot]andAndy Lee 19b6842b21 chore: bump version to v1.6.0 (#480) (#481)
(cherry picked from commit d175e3b11a)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-26 11:22:02 +08:00
mergify[bot]andAndy Lee fa78229d3a fix: resource page detail title link (#459) (#464)
* fix: resource page detail title link



* refactor: extract harvester type in a map



* refactor: put in config/harvester-map.js



---------


(cherry picked from commit 795e5d178f)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-15 16:54:00 +08:00
mergify[bot]andAndy Lee c3bfaf00ea fix: spec.domain.memory.guest is lost issue (#462) (#463)
(cherry picked from commit 2e03ac3cf7)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-15 12:02:18 +08:00
mergify[bot]andAndy Lee 1a176960bc refactor: remove unneeded divider in storage class action menu (#460) (#461)
(cherry picked from commit 76605fdc07)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-15 10:53:56 +08:00
mergify[bot]andAndy Lee e82ba9d9f5 chore: bump version to v1.6.0-rc6 (#456) (#457)
(cherry picked from commit c289b001db)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-13 15:16:55 +08:00
mergify[bot]andYiya Chen f7fc001abd fix: fallback default value (#454) (#455)
(cherry picked from commit e8d63da1eb)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-08-12 13:55:37 +08:00
mergify[bot]andAndy Lee d1c676ee2c chore: update reviewer (#452) (#453)
(cherry picked from commit 4a456ac07d)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-05 12:41:13 +08:00
mergify[bot]andAndy Lee c562dd73ba chore: bump to 1.6.0-rc5 (#450) (#451)
(cherry picked from commit 57dfddd593)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-04 21:34:33 +08:00
mergify[bot]andAndy Lee 1cd5e5ca7d feat: add restore failed message in VM state description (#448) (#449)
(cherry picked from commit 66eac96575)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-04 14:11:58 +08:00
mergify[bot]andAndy Lee 1aaa8f71c4 fix: featureFlags is undefined when installing lower version of ui-ext (#443) (#447)
* fix: featureFlags is undefined if install lower version of ui-ext

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

(cherry picked from commit 182d92d80b)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-01 15:41:10 +08:00
mergify[bot]andYiya Chen 003a09d0a9 fix: update cdiSettings (#444) (#445)
(cherry picked from commit 1b362f570b)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-08-01 12:15:10 +08:00
mergify[bot]andAndy Lee f08e632aad chore: bump version to v1.6.0-rc4 (#439) (#442)
* chore: bump version to v1.6.0-rc4



* fix: missing vm network migration setting



---------


(cherry picked from commit cf798048f8)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-30 16:07:47 +08:00
mergify[bot]andAndy Lee df1d7001bc fix: cpu/ memory columns in host detail VM tab (#436) (#437)
(cherry picked from commit 93c8399935)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-30 11:57:52 +08:00
mergify[bot]andAndy Lee 41f78c83a7 fix: isStopping state condition (#431) (#435)
* fix: isStopping state condition



* refactor: remove unused variable



---------


(cherry picked from commit b72f523ddf)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-29 13:02:30 +08:00
mergify[bot]andAndy Lee f1a53311ce chore: upgrade rancher/shell to 3.0.5-rc.7 (#429) (#432)
(cherry picked from commit e5ffb08df3)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-29 10:00:10 +08:00
mergify[bot]andAndy Lee e0c6e905a9 refactor: filter anchor link in setting description when searching (#428) (#430)
(cherry picked from commit 98a6322c11)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-28 18:26:19 +08:00
mergify[bot]andAndy Lee 1852e434f1 feat: add runStragety field when cloning a VM (#424) (#427)
* feat: add runStragety field when cloning a VM



* refactor: remove unneeded change



---------


(cherry picked from commit be7e4bd80b)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-28 14:31:18 +08:00
mergify[bot] 77c085d3a7 feat: expose CDI settings (#418) (#425)
* feat: add cdi settings tab



* feat: basic layout



* feat: add fields mutation



* refactor: rename keys



* refactor: add edit mode



* refactor: remove isCreate



* feat: filter volume mode options



---------

(cherry picked from commit ed2bc3100b)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-25 13:59:50 +08:00
mergify[bot]andYiya Chen 9dbe7d9015 fix: exception error (#422) (#423)
(cherry picked from commit b6ffb3e9f1)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-24 13:00:44 +08:00
mergify[bot]andAndy Lee 3f19494d98 chore: bump version to 1.6.0-rc3 (#420) (#421)
(cherry picked from commit 3d8a6bba7e)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-23 15:11:41 +08:00
mergify[bot]andAndy Lee 296601d38c feat: CPU / Memory hotplug support (#413) (#419)
* feat: add maxCPU and maxMemory



* feat: add hotplug dialog



* feat: add restart message



* feat: let VM template support cpuMemoryHotplug



* feat: add feature flag



* feat: add max-hotplug-ratio setting



---------


(cherry picked from commit be9311dc0c)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-23 14:11:08 +08:00
mergify[bot]andAndy Lee 5d9ba584b8 refactor: remove unneeded translation keys (#416) (#417)
(cherry picked from commit e294f4c00f)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-21 17:47:32 +08:00
eab4ba3d0f feat: Remove guest cluster in Rancher (#391) (#415)
* feat: Remove guest cluster in Rancher



* feat: add feature flag 1.6.0



* feat: update for review



* chore: fix for review



* chore: fix for review



* refactor: reduce redundant code



* chore: change text area to yaml editor



* refactor: change radio and yaml editor position



---------




(cherry picked from commit a9fa928912)

Signed-off-by: Nick Chung <nick.chung@suse.com>
Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Nick Chung <Nick.Chung@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-21 17:28:34 +08:00
mergify[bot]andYiya Chen 4e5320dc72 feat: disable vm volume resize (#409) (#412)
(cherry picked from commit ef2b4d1589)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-17 18:33:15 +08:00
mergify[bot]andAndy Lee ba7092f400 ci: disable subject-max-length commitlint rule (#408) (#411)
(cherry picked from commit a73e9f0ac1)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-17 13:54:15 +08:00
mergify[bot]andNick Chung eec7c3573d chore: disable vmstate-persistence and longhorn-static StorageClasses (backport #377) (#405)
* chore: disable vmstate-persistence and longhorn-static StorageClasses (#377)

* chore: disable vmstate-persistence and longhorn-static StorageClasses

Signed-off-by: Nick Chung <nick.chung@suse.com>

* chore: allow internal storage class deletions in image and volumn

Signed-off-by: Nick Chung <nick.chung@suse.com>

* chore: remove deletion tooltips in image and volume pages

Signed-off-by: Nick Chung <nick.chung@suse.com>

* chore: rollback style changes of image and volume lists

Signed-off-by: Nick Chung <nick.chung@suse.com>

---------

Signed-off-by: Nick Chung <nick.chung@suse.com>
(cherry picked from commit b775ce5f50)

* chore: fix missing semicolon in types.js

Signed-off-by: Nick Chung <Nick.Chung@suse.com>

---------

Signed-off-by: Nick Chung <nick.chung@suse.com>
Signed-off-by: Nick Chung <Nick.Chung@suse.com>
Co-authored-by: Nick Chung <Nick.Chung@suse.com>
2025-07-16 18:32:13 +08:00
mergify[bot]andAndy Lee f0014cc627 chore: bump version to v1.6.0-rc2 (#403) (#404)
(cherry picked from commit 193d4536e3)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-16 18:13:20 +08:00
mergify[bot]andAndy Lee 9aa311a7e3 feat: support for subnets and VPCs from UI (#374) (#402)
(cherry picked from commit 15950eb209165a980ec53d22fe2898b94d6b1c8b)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-16 17:48:48 +08:00
mergify[bot]andYiya Chen 013d9f1d7e fix: add default value to prevent exception (#397) (#401)
(cherry picked from commit bcabefe9f3)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-16 15:19:21 +08:00
mergify[bot]andAndy Lee 8c044ba44c ci: add package version check (#396) (#400)
* ci: add package version check



* ci: extract check version in a script



* ci: update env variable



---------


(cherry picked from commit c541f81dc3)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-16 13:21:09 +08:00
mergify[bot]andYiya Chen 7b8592e07d feat: add vm-migration-network setting (#395) (#398)
(cherry picked from commit 4486f71c8f)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-16 10:22:31 +08:00
mergify[bot]andYiya Chen 53d2e67b4f chore: upgrade rancher/shell to 3.0.5-rc6 (#393) (#394)
* chore: bump shell rc6
* chore: add rancher restriction

---------


(cherry picked from commit 060105ead3)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-14 10:01:12 +08:00
mergify[bot]andAndy Lee 1d146a40bd fix: find the correct vmim based on creationTimeStamp (#389) (#390)
(cherry picked from commit 3ea73978ee)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-10 11:22:48 +08:00
mergify[bot]andYiya Chen 11d6943596 feat: add csi-online-expand-validation setting (#378) (#388)
* feat: add csi-online-expand-validation setting
* feat: invalid json error message
* feat: handle API errors
* refactor: remove inStore()

---------


(cherry picked from commit f4e363396d)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-09 15:31:17 +08:00
mergify[bot]andNick Chung 7b754457ec chore: remove run-lint from build-standalone-on-release (#355) (#387)
(cherry picked from commit 7386a2deb6)

Signed-off-by: Nick Chung <nick.chung@suse.com>
Co-authored-by: Nick Chung <Nick.Chung@suse.com>
2025-07-09 14:51:47 +08:00
mergify[bot]andAndy Lee 2cb838eded chore: bump version to v1.6.0-rc1 (#385) (#386)
(cherry picked from commit fa16e24983)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-09 14:36:39 +08:00
mergify[bot]andYiya Chen bdcb34ab14 ci: ignore draft PR add add backport labels (#382) (#384)
* ci: skip draft PR
* ci: fix auto assign
* ci: add backport label
* fix: remove token

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
(cherry picked from commit ee28697161)

# Conflicts:
#	.github/workflows/backport-label.yaml

Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-09 13:28:54 +08:00
mergify[bot]andAndy Lee 8daf44b3f6 fix: read volume accessMode even is RWO (#381) (#383)
(cherry picked from commit ce63ee6a19)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-09 12:07:41 +08:00
mergify[bot]andYiya Chen 8044e74102 feat: disable vm images (#376) (#380)
(cherry picked from commit 85a06feb91)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-08 17:10:14 +08:00
mergify[bot]andNick Chung 264d65cc6d fix: can't migrate network config in Rancher integration (#371) (#373)
(cherry picked from commit 0ef4ff65cc)

Signed-off-by: Nick Chung <nick.chung@suse.com>
Co-authored-by: Nick Chung <Nick.Chung@suse.com>
2025-07-03 16:59:13 +08:00
mergify[bot]andAndy Lee 342a82c8e0 fix: add custom promptRemove for backup and snasphot deletion (#367) (#372)
(cherry picked from commit f12717a8f4)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-03 14:06:40 +08:00
mergify[bot]andNick Chung e7cd5cc4b0 feat: [ENHANCEMENT] Add setting upgrade-config to UI(#8369) (#347) (#363)
(cherry picked from commit dc683a50a4)

Signed-off-by: Nick Chung <nick.chung@suse.com>
Co-authored-by: Nick Chung <Nick.Chung@suse.com>
2025-07-01 16:24:50 +08:00
mergify[bot]andAndy Lee cf405be71e fix: cloned secret should keep originl type (#354) (#362)
(cherry picked from commit 1d89aafeab)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-01 11:47:40 +08:00
mergify[bot]andYiya Chen 911d30d7ba feat: SB Enhancements (NS selection and timeout) (#345) (#360)
* feat: add namespace field
* feat: add optional inputs
* feat: refine code
* feat: add feature flag
* refactor: fix lint error
* feat: filter default namespaces
* refactor: hide tips with feature flag
* refactor: use UnitInput
* feat: load default value from settings
* refactor: fix API url
* refactor: no available namespaces
* chore: update subject-case rule
---------
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

(cherry picked from commit be421054d8)

Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-01 09:53:30 +08:00
mergify[bot]andAndy Lee 1b4bfa0962 ci: replace tag with github.event.release.tag_name (#356) (#359)
(cherry picked from commit fcef0391bb)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-06-30 11:01:19 +08:00
Andy Lee d0f6f18a8a docs: update readme.md to include v1.6-head
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-26 11:55:34 +08:00
60 changed files with 914 additions and 3610 deletions
-1
View File
@@ -14,4 +14,3 @@ dist-pkg
pkg/harvester/index.ts
pkg/harvester/store/*
scripts/pkgfile.js
tmp
+31 -12
View File
@@ -1,14 +1,33 @@
pull_request_rules:
- name: Ask to resolve conflict
conditions:
- conflict
actions:
comment:
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
- name: Automatically open backport PR to release-harvester-v1.5
conditions:
- base=main
- label="require backport 1.5"
actions:
backport:
branches:
- "release-harvester-v1.5"
assignees:
- "{{ author }}"
labels:
- "v1.5 backport PR"
commands_restrictions:
backport:
conditions:
- or:
- sender-permission>=write
- sender=github-actions[bot]
- name: Automatically open backport PR to release-harvester-v1.0
conditions:
- base=main
- label="require backport 1.0"
actions:
backport:
branches:
- "release-harvester-v1.0"
assignees:
- "{{ author }}"
labels:
- "v1.0 backport PR"
- name: Ask to resolve conflict
conditions:
- conflict
actions:
comment:
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"extends": [
"config:recommended",
":semanticCommitTypeAll(deps)",
":semanticCommitScopeDisabled"
-49
View File
@@ -1,49 +0,0 @@
name: "[PR Management] Request Backport via Mergify"
on:
pull_request_target:
types: [closed]
branches: [main]
permissions:
pull-requests: write
jobs:
comment-backport:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.base_ref }}
- name: Post Mergify backport command
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
pr_number=${{ github.event.pull_request.number }}
repo="${{ github.repository }}"
labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
labels=$(echo "$labels_json" | jq -r '.[] // empty')
echo "Labels on PR: $labels"
matches=$(echo "$labels" | grep -oE '^require backport/v[0-9]+\.[0-9]+$' || true)
if [[ -z "$matches" ]]; then
echo "No back‑port labels found — skipping."
exit 0
fi
branches=$(echo "$matches" \
| sed -E 's/^require backport\/v/release-harvester-v/' \
| sort -u | tr '\n' ' ')
branches=${branches%% }
cmd="@Mergifyio backport $branches"
echo "Posting Mergify command: $cmd"
gh pr comment "$pr_number" --repo "$repo" --body "$cmd"
+3 -17
View File
@@ -41,14 +41,8 @@ jobs:
FROM="$GITHUB_BASE_SHA"
TO="$GITHUB_HEAD_SHA"
elif [ -n "$GITHUB_BEFORE" ] && [ -n "$GITHUB_AFTER" ]; then
if [ "$GITHUB_BEFORE" = "0000000000000000000000000000000000000000" ]; then
# first push to HEAD
FROM=""
TO="$GITHUB_AFTER"
else
FROM="$GITHUB_BEFORE"
TO="$GITHUB_AFTER"
fi
FROM="$GITHUB_BEFORE"
TO="$GITHUB_AFTER"
else
echo "No valid commit range found, skipping commitlint."
exit 0
@@ -56,15 +50,7 @@ jobs:
echo "FROM=$FROM"
echo "TO=$TO"
if [ -z "$FROM" ]; then
echo "Linting last commit $TO"
npx commitlint --last --verbose
else
echo "Linting commits from $FROM to $TO"
npx commitlint --from "$FROM" --to "$TO" --verbose
fi
npx commitlint --from "$FROM" --to "$TO" --verbose
env:
GITHUB_EVENT_NAME: ${{ github.event_name }}
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
+1
View File
@@ -138,6 +138,7 @@ To validate changes in a release series, switch to the appropriate `vX.Y-head` b
- Examples:
- Test `1.0.x` series → `v1.0-head`
- Test `1.5.x` series → `v1.5-head`
- Test `1.6.x` series → `v1.6-head`
**Steps:**
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
-4
View File
@@ -22,10 +22,6 @@ module.exports = {
'security', // Security fixes
]
],
// Allows the scope to be empty. [0] means the rule is disabled.
'scope-empty': [0, 'always'],
// Allows any string for the scope.
'scope-case': [2, 'always', 'kebab-case'],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'subject-case': [0, 'never'],
+5 -5
View File
@@ -1,13 +1,13 @@
{
"name": "harvester-ui-extension",
"version": "1.7.0-rc4",
"version": "1.6.1",
"private": false,
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@babel/plugin-transform-class-static-block": "7.28.3",
"@rancher/shell": "3.0.8-rc.8",
"@rancher/shell": "3.0.5-rc.8",
"cache-loader": "^4.1.0",
"color": "4.2.3",
"ip": "2.0.1",
@@ -24,12 +24,12 @@
"glob": "7.2.3",
"glob-parent": "6.0.2",
"json5": "2.2.3",
"@types/lodash": "4.17.21",
"@types/lodash": "4.17.20",
"merge": "2.1.1",
"node-forge": "1.3.2",
"node-forge": "1.3.1",
"nth-check": "2.1.1",
"qs": "6.14.0",
"roarr": "7.21.2",
"roarr": "7.21.1",
"semver": "7.7.3",
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
},
+1
View File
@@ -11,6 +11,7 @@ const CATEGORY = {
ui: [
'branding',
'ui-source',
'ui-plugin-index',
'ui-index',
]
};
@@ -257,10 +257,6 @@ export default {
});
},
reconnect() {
this.$refs.novncConsole.reconnect();
},
softReboot() {
this.vmResource.doSoftReboot();
},
@@ -310,13 +306,6 @@ export default {
{{ t("harvester.action.softreboot") }}
</button>
<button
class="btn btn-sm bg-primary"
@click="reconnect"
>
{{ t("harvester.action.reconnect") }}
</button>
<v-dropdown
v-if="!hideCustomKeysBar"
ref="customKeyPopover"
@@ -391,7 +380,4 @@ export default {
.vm-console .v-popper__popper{
margin-top: 8px;
}
.vm-console .v-popper__inner{
overflow-y: visible;
}
</style>
@@ -1,154 +0,0 @@
<script>
import { LabeledInput } from '@components/Form/LabeledInput';
import { RadioGroup } from '@components/Form/Radio';
import UnitInput from '@shell/components/form/UnitInput';
import { MEBIBYTE } from '../../utils/unit';
import { Banner } from '@components/Banner';
export default {
name: 'KubevirtMigration',
components: {
LabeledInput,
UnitInput,
RadioGroup,
Banner
},
props: {
registerBeforeHook: {
type: Function,
required: true
},
value: {
type: Object,
default: () => ({
value: '',
default: '{}'
})
},
},
data() {
const migration = this.parseJSON(this.value?.value) || this.parseJSON(this.value?.default) || {};
return {
MEBIBYTE,
migration,
parseError: null,
};
},
created() {
this.registerBeforeHook?.(this.willSave, 'willSave');
},
methods: {
parseJSON(string) {
try {
return JSON.parse(string);
} catch (e) {
this.parseError = this.t('kubevirtMigration.parseError', { error: e.message });
// eslint-disable-next-line no-console
console.error('Failed to parse JSON:', e.message);
return null;
}
},
updateValue() {
if (this.value) {
this.value.value = JSON.stringify(this.migration);
}
},
useDefault() {
if (this.value?.default) {
const defaultMigration = this.parseJSON(this.value.default) || {};
this.migration = defaultMigration;
this.updateValue();
}
},
async willSave() {
this.updateValue();
return Promise.resolve();
},
},
};
</script>
<template>
<div>
<Banner
v-if="parseError"
color="error"
>
{{ parseError }}
</Banner>
<div class="migration-field">
<LabeledInput
v-model:value.number="migration.parallelMigrationsPerCluster"
:label="t('harvester.setting.kubevirtMigration.parallelMigrationsPerCluster')"
type="number"
min="1"
/>
<LabeledInput
v-model:value.number="migration.parallelOutboundMigrationsPerNode"
:label="t('harvester.setting.kubevirtMigration.parallelOutboundMigrationsPerNode')"
type="number"
min="1"
/>
<UnitInput
v-model:value="migration.bandwidthPerMigration"
min="0"
:label="t('harvester.setting.kubevirtMigration.bandwidthPerMigration')"
:mode="mode"
:suffix="MEBIBYTE"
:tooltip="t('harvester.setting.kubevirtMigration.bandwidthPerMigrationTooltip', _, true)"
/>
<UnitInput
v-model:value="migration.completionTimeoutPerGiB"
:label="t('harvester.setting.kubevirtMigration.completionTimeoutPerGiB')"
:mode="mode"
:suffix="migration.completionTimeoutPerGiB === 1 ? 'Second' : 'Seconds'"
min="10"
/>
<UnitInput
v-model:value="migration.progressTimeout"
:label="t('harvester.setting.kubevirtMigration.progressTimeout')"
:mode="mode"
:suffix="migration.progressTimeout === 1 ? 'Second' : 'Seconds'"
min="10"
/>
<div
v-for="field in ['allowAutoConverge','allowPostCopy','unsafeMigrationOverride','allowWorkloadDisruption','disableTLS','matchSELinuxLevelOnMigration']"
:key="field"
>
<label
class="mb-5"
:for="field"
>{{ t(`harvester.setting.kubevirtMigration.${field}`) }}</label>
<RadioGroup
:id="field"
v-model:value="migration[field]"
:options="[
{ label: t('advancedSettings.edit.trueOption'), value: true },
{ label: t('advancedSettings.edit.falseOption'), value: false },
]"
/>
</div>
</div>
</div>
</template>
<style scoped>
.migration-field {
display: flex;
flex-direction: column;
gap: 12px;
}
</style>
@@ -18,20 +18,16 @@ export default {
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
if (this.customSupportBundleFeatureEnabled) {
try {
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
const response = await this.$store.dispatch('harvester/request', { url });
try {
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
const response = await this.$store.dispatch('harvester/request', { url });
this.defaultNamespaces = response.data || [];
} catch (error) {
this.defaultNamespaces = [];
}
} else {
this.defaultNamespaces = response.data || [];
} catch (error) {
this.defaultNamespaces = [];
} finally {
this.loading = false;
}
this.loading = false;
},
data() {
@@ -46,38 +42,24 @@ export default {
},
computed: {
customSupportBundleFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
},
allNamespaces() {
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
},
filteredNamespaces() {
if (!this.customSupportBundleFeatureEnabled) {
return this.allNamespaces;
}
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
},
namespaceOptions() {
const mappedNamespaces = this.filteredNamespaces.map((ns) => ({ label: ns, value: ns }));
if (!this.customSupportBundleFeatureEnabled) {
return mappedNamespaces;
}
const allSelected =
this.namespaces.length === this.filteredNamespaces.length &&
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
return [controlOption, ...mappedNamespaces];
return [controlOption, ...this.filteredNamespaces];
}
},
+7 -10
View File
@@ -1,13 +1,10 @@
// suffix of doc link, see utils/feature-flags.js how to get complete doc link
export const DOC = {
CONSOLE_URL: `/host/#remote-console`,
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
UPGRADE_URL: `/upgrade/index`,
UPGRADE_CONFIG_URL: `/advanced/index#upgrade-config`,
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
VPC_CONFIGURATION_EXAMPLES: `/networking/kubeovn-vpc#vpc-peering-configuration-examples`,
NETWORK_POLICY: `/networking/kubeovn-vm-isolation/#network-policies`,
TRANSPARENT_HUGEPAGES: `https://docs.kernel.org/admin-guide/mm/transhuge.html`,
CONSOLE_URL: `/host/#remote-console`,
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
UPGRADE_URL: `/upgrade/index`,
UPGRADE_CONFIG_URL: `/advanced/index#upgrade-config`,
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
};
+1 -7
View File
@@ -39,6 +39,7 @@ const FEATURE_FLAGS = {
],
'v1.5.1': [],
'v1.6.0': [
'vmMachineTypes',
'customSupportBundle',
'csiOnlineExpandValidation',
'vmNetworkMigration',
@@ -49,13 +50,6 @@ const FEATURE_FLAGS = {
'vmCloneRunStrategy',
],
'v1.6.1': [],
'v1.7.0': [
'vmMachineTypeAuto',
'lhV2VolExpansion',
'l2VlanTrunkMode',
'kubevirtMigration',
'hotplugNic'
]
};
const generateFeatureFlags = () => {
-38
View File
@@ -10,7 +10,6 @@ import {
LOGGING,
STORAGE_CLASS,
SECRET,
NETWORK_POLICY
} from '@shell/config/types';
import { HCI, VOLUME_SNAPSHOT } from '../types';
import {
@@ -426,7 +425,6 @@ export function init($plugin, store) {
HCI.CLUSTER_NETWORK,
HCI.NETWORK_ATTACHMENT,
HCI.VPC,
NETWORK_POLICY,
HCI.LB,
HCI.IP_POOL,
],
@@ -457,7 +455,6 @@ export function init($plugin, store) {
HCI.PCI_DEVICE,
HCI.SR_IOVGPU_DEVICE,
HCI.VGPU_DEVICE,
HCI.MIG_CONFIGURATION,
HCI.USB_DEVICE,
HCI.ADD_ONS,
HCI.SECRET,
@@ -569,21 +566,6 @@ export function init($plugin, store) {
ifHaveType: HCI.VPC,
});
configureType(NETWORK_POLICY, { hiddenNamespaceGroupButton: true, canYaml: false });
virtualType({
labelKey: 'harvester.networkPolicy.label',
name: NETWORK_POLICY,
namespaced: true,
weight: 186,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: NETWORK_POLICY }
},
exact: false,
ifHaveType: NETWORK_POLICY,
});
configureType(HCI.SNAPSHOT, {
isCreatable: false,
location: {
@@ -850,26 +832,6 @@ export function init($plugin, store) {
]
});
virtualType({
labelKey: 'harvester.migconfiguration.label',
group: 'advanced',
weight: 12,
name: HCI.MIG_CONFIGURATION,
namespaced: false,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.MIG_CONFIGURATION }
},
exact: false,
ifHaveType: HCI.MIG_CONFIGURATION,
});
configureType(HCI.MIG_CONFIGURATION, {
isCreatable: false,
hiddenNamespaceGroupButton: true,
canYaml: false,
});
virtualType({
labelKey: 'harvester.usb.label',
group: 'advanced',
+3 -5
View File
@@ -4,6 +4,7 @@ export const HCI_SETTING = {
LOG_LEVEL: 'log-level',
SERVER_VERSION: 'server-version',
UI_INDEX: 'ui-index',
UI_PLUGIN_INDEX: 'ui-plugin-index',
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
UPGRADE_CHECKER_URL: 'upgrade-checker-url',
VLAN: 'vlan',
@@ -37,8 +38,7 @@ export const HCI_SETTING = {
UPGRADE_CONFIG: 'upgrade-config',
VM_MIGRATION_NETWORK: 'vm-migration-network',
RANCHER_CLUSTER: 'rancher-cluster',
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
KUBEVIRT_MIGRATION: 'kubevirt-migration'
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio'
};
export const HCI_ALLOWED_SETTINGS = {
@@ -84,6 +84,7 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES]: { from: 'import', canReset: true },
[HCI_SETTING.AUTO_DISK_PROVISION_PATHS]: { canReset: true },
[HCI_SETTING.RELEASE_DOWNLOAD_URL]: { kind: 'url' },
[HCI_SETTING.UI_PLUGIN_INDEX]: { kind: 'url' },
[HCI_SETTING.CONTAINERD_REGISTRY]: {
kind: 'json', from: 'import', canReset: true
},
@@ -116,9 +117,6 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.VM_MIGRATION_NETWORK]: {
kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration',
},
[HCI_SETTING.KUBEVIRT_MIGRATION]: {
kind: 'json', from: 'import', canReset: true, featureFlag: 'kubevirtMigration',
}
};
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
+3 -9
View File
@@ -4,10 +4,9 @@ export const BACKUP_TYPE = {
};
export const NETWORK_TYPE = {
L2VLAN: 'L2VlanNetwork',
UNTAGGED: 'UntaggedNetwork',
OVERLAY: 'OverlayNetwork',
L2TRUNK_VLAN: 'L2VlanTrunkNetwork',
L2VLAN: 'L2VlanNetwork',
UNTAGGED: 'UntaggedNetwork',
OVERLAY: 'OverlayNetwork',
};
export const VOLUME_MODE = {
@@ -24,8 +23,3 @@ export const INTERNAL_STORAGE_CLASS = {
VMSTATE_PERSISTENCE: 'vmstate-persistence',
LONGHORN_STATIC: 'longhorn-static',
};
export const L2VLAN_MODE = {
ACCESS: 'access',
TRUNK: 'trunk',
};
@@ -1,124 +0,0 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import SortableTable from '@shell/components/SortableTable';
export default {
components: {
ResourceTabs,
Tab,
SortableTable,
},
mixins: [CreateEditView],
props: {
value: {
type: Object,
default: () => {
return {};
}
}
},
computed: {
headers() {
return [
{
name: 'profileName',
labelKey: 'harvester.migconfiguration.tableHeaders.profileName',
value: 'name',
width: 75,
sort: 'name',
dashIfEmpty: true,
},
{
name: 'vGPUID',
labelKey: 'harvester.migconfiguration.tableHeaders.vGPUID',
value: 'vGPUID',
width: 75,
sort: 'vGPUID',
dashIfEmpty: true,
},
{
name: 'available',
labelKey: 'harvester.migconfiguration.tableHeaders.available',
value: 'available',
width: 75,
sort: 'available',
align: 'center',
dashIfEmpty: true,
},
{
name: 'requested',
labelKey: 'harvester.migconfiguration.tableHeaders.requested',
value: 'requested',
width: 75,
sort: 'requested',
align: 'center',
dashIfEmpty: true,
},
{
name: 'total',
labelKey: 'harvester.migconfiguration.tableHeaders.total',
value: 'total',
width: 75,
sort: 'total',
align: 'center',
dashIfEmpty: true,
},
];
},
rows() {
let out = (this.value?.status?.profileStatus || []).map((profile) => {
const {
id, name, total, available
} = profile;
return {
id,
name,
total,
available,
vGPUID: profile.vGPUID?.join(', ') || '',
};
});
out = out.map((row) => {
const requested = this.value?.spec?.profileSpec.find((p) => p.id === row.id)?.requested || 0;
return { ...row, requested };
});
return out;
},
},
};
</script>
<template>
<ResourceTabs
:value="value"
:need-events="false"
:need-related="false"
:mode="mode"
>
<Tab
name="Profile Status"
:label="t('harvester.migconfiguration.profileStatus')"
>
<SortableTable
:headers="headers"
:rows="rows"
key-field="condition"
default-sort-by="condition"
:table-actions="false"
:row-actions="false"
:search="false"
/>
</Tab>
</ResourceTabs>
</template>
@@ -1,119 +0,0 @@
<script>
import LabelValue from '@shell/components/LabelValue';
import { HCI } from '../../types';
import { DOC } from '../../config/doc-links';
export default {
name: 'HarvesterHugepages',
components: { LabelValue },
props: {
node: {
type: Object,
required: true,
},
},
computed: {
docsTransparentHugepagesLink() {
return DOC.TRANSPARENT_HUGEPAGES;
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
this.hugepages = hash.find((node) => {
return node.id === this.node.id;
}) || {};
},
data() {
return { hugepages: {} };
},
};
</script>
<template>
<div>
<template v-if="hugepages.status">
<h2>{{ t('harvester.host.hugepages.meminfo') }}</h2>
<div class="row mb-20">
<div class="col span-6">
<LabelValue
:name="t('harvester.host.hugepages.status.anon')"
:value="hugepages.status.meminfo.anonHugePages"
/>
</div>
<div class="col span-6">
<LabelValue
:name="t('harvester.host.hugepages.status.size')"
:value="hugepages.status.meminfo.hugepageSize"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-3">
<LabelValue
:name="t('harvester.host.hugepages.status.total')"
:value="hugepages.status.meminfo.hugePagesTotal"
/>
</div>
<div class="col span-3">
<LabelValue
:name="t('harvester.host.hugepages.status.free')"
:value="hugepages.status.meminfo.hugePagesFree"
/>
</div>
<div class="col span-3">
<LabelValue
:name="t('harvester.host.hugepages.status.rsvd')"
:value="hugepages.status.meminfo.hugePagesRsvd"
/>
</div>
<div class="col span-3">
<LabelValue
:name="t('harvester.host.hugepages.status.surp')"
:value="hugepages.status.meminfo.hugePagesSurp"
/>
</div>
</div>
<div>
<hr class="divider" />
<h3>
<t
k="harvester.host.hugepages.transparent.title"
:raw="true"
:url="docsTransparentHugepagesLink"
/>
</h3>
<div class="row mb-20">
<div class="col span-4">
<LabelValue
:name="t('harvester.host.hugepages.transparent.enabled')"
:value="hugepages.spec.transparent.enabled"
/>
</div>
<div class="col span-4">
<LabelValue
:name="t('harvester.host.hugepages.transparent.shmemEnabled')"
:value="hugepages.spec.transparent.shmemEnabled"
/>
</div>
<div class="col span-4">
<LabelValue
:name="t('harvester.host.hugepages.transparent.defrag')"
:value="hugepages.spec.transparent.defrag"
/>
</div>
</div>
</div>
</template>
</div>
</template>
@@ -27,7 +27,6 @@ import Instance from './VirtualMachineInstance';
import Disk from './HarvesterHostDisk';
import VlanStatus from './VlanStatus';
import HarvesterKsmtuned from './HarvesterKsmtuned.vue';
import HarvesterHugepages from './HarvesterHugepages.vue';
import HarvesterSeeder from './HarvesterSeeder';
const LONGHORN_SYSTEM = 'longhorn-system';
@@ -47,7 +46,6 @@ export default {
VlanStatus,
LabelValue,
HarvesterKsmtuned,
HarvesterHugepages,
Loading,
SortableTable,
HarvesterSeeder,
@@ -211,12 +209,6 @@ export default {
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
},
hasHugepagesSchema() {
const inStore = this.$store.getters['currentProduct'].inStore;
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.HUGEPAGES);
},
hasBlockDevicesSchema() {
return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE);
},
@@ -476,16 +468,6 @@ export default {
/>
</Tab>
<Tab
v-if="hasHugepagesSchema"
name="hugepages"
:weight="0"
:show-header="false"
:label="t('harvester.host.tabs.hugepages')"
>
<HarvesterHugepages :node="value" />
</Tab>
<Tab
v-if="seederEnabled"
name="seeder"
@@ -213,7 +213,6 @@ export default {
const diskRows = this.getDiskRows(neu);
this['diskRows'] = diskRows;
this['networkRows'] = this.getNetworkRows(neu, { fromTemplate: false, init: false });
},
deep: true
}
@@ -266,7 +265,6 @@ export default {
<Network
v-model:value="networkRows"
mode="view"
:vm="value"
/>
</Tab>
@@ -32,7 +32,7 @@ export default {
resources: {
type: Array,
required: true
},
}
},
data() {
@@ -43,7 +43,7 @@ export default {
...mapState('action-menu', ['modalData']),
title() {
return this.modalData?.title || 'dialog.promptRemove.title';
return this.modalData.title || 'dialog.promptRemove.title';
},
formattedType() {
@@ -51,7 +51,7 @@ export default {
},
warningMessage() {
if (this.modalData?.warningMessage) return this.modalData.warningMessage;
if (this.modalData.warningMessage) return this.modalData.warningMessage;
const isPlural = this.type.endsWith('s');
const thisOrThese = isPlural ? 'these' : 'this';
@@ -145,7 +145,6 @@ export default {
try {
for (const resource of this.resources) {
await resource.remove();
if (this.modalData?.extraActionAfterRemove) await this.modalData.extraActionAfterRemove();
}
buttonDone(true);
this.close();
@@ -11,7 +11,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default {
name: 'HotplugVolumeModal',
name: 'HotplugModal',
emits: ['close'],
@@ -62,7 +62,7 @@ export default {
return false;
}
return true;
return !pvc.attachVM;
})
.map((pvc) => {
return {
@@ -90,7 +90,7 @@ export default {
if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', {
title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.modal.hotplugVolume.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
message: this.t('harvester.modal.hotplug.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
}, { root: true });
this.close();
@@ -122,7 +122,7 @@ export default {
>
<template #title>
<h4
v-clean-html="t('harvester.modal.hotplugVolume.title')"
v-clean-html="t('harvester.modal.hotplug.title')"
class="text-default-text"
/>
</template>
@@ -1,212 +0,0 @@
<script>
import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex';
import { NETWORK_ATTACHMENT } from '@shell/config/types';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { NETWORK_TYPE } from '../config/types';
export default {
name: 'AddHotplugNic',
emits: ['close'],
components: {
AsyncButton,
Card,
LabeledInput,
LabeledSelect,
Banner
},
props: {
resources: {
type: Array,
required: true
}
},
async fetch() {
try {
this.allVMNetworks = await this.$store.dispatch('harvester/findAll', { type: NETWORK_ATTACHMENT });
} catch (err) {
this.errors = exceptionToErrorsArray(err);
this.allVMNetworks = [];
}
},
data() {
return {
interfaceName: '',
networkName: '',
macAddress: '',
allVMNetworks: [],
errors: [],
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources?.[0];
},
isFormValid() {
return this.interfaceName !== '' && this.networkName !== '';
},
vmNetworksOption() {
return this.allVMNetworks
.filter((network) => {
const labels = network.metadata?.labels || {};
const type = labels[HCI_ANNOTATIONS.NETWORK_TYPE];
const isValidType = [
NETWORK_TYPE.L2VLAN,
NETWORK_TYPE.UNTAGGED,
NETWORK_TYPE.L2TRUNK_VLAN,
].includes(type);
return isValidType && !network.isSystem;
})
.map((network) => {
const label = network.isNotReady ? `${ network.id } (${ this.t('generic.notReady') })` : network.id;
return ({
label,
value: network.id || '',
disabled: network.isNotReady,
});
});
}
},
methods: {
close() {
this.interfaceName = '';
this.networkName = '';
this.macAddress = '';
this.errors = [];
this.$emit('close');
},
async save(buttonCb) {
if (!this.actionResource) {
buttonCb(false);
return;
}
const payload = {
interfaceName: this.interfaceName,
networkName: this.networkName
};
if (this.macAddress) {
payload.macAddress = this.macAddress;
}
try {
const res = await this.actionResource.doAction('addNic', payload);
if ([200, 204].includes(res?._status)) {
this.$store.dispatch('growl/success', {
title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.modal.hotplugNic.success', {
interfaceName: this.interfaceName,
vm: this.actionResource.nameDisplay
})
}, { root: true });
this.close();
buttonCb(true);
} else {
this.errors = exceptionToErrorsArray(res);
buttonCb(false);
}
} catch (err) {
this.errors = exceptionToErrorsArray(err);
buttonCb(false);
}
}
}
};
</script>
<template>
<Card
ref="modal"
name="modal"
:show-highlight-border="false"
>
<template #title>
<h4
v-clean-html="t('harvester.modal.hotplugNic.title')"
class="text-default-text"
/>
</template>
<template #body>
<LabeledInput
v-model:value="interfaceName"
:label="t('generic.name')"
required
/>
<LabeledSelect
v-model:value="networkName"
class="mt-20"
:label="t('harvester.modal.hotplugNic.vmNetwork')"
:options="vmNetworksOption"
required
/>
<LabeledInput
v-model:value="macAddress"
class="mt-20"
label-key="harvester.modal.hotplugNic.macAddress"
:tooltip="t('harvester.modal.hotplugNic.macAddressTooltip', _, true)"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
:label="err"
color="error"
/>
</template>
<template #actions>
<div class="actions">
<div class="buttons">
<button
type="button"
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="apply"
:disabled="!isFormValid"
@click="save"
/>
</div>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -1,12 +1,13 @@
<script>
import { mapState, mapGetters } from 'vuex';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
export default {
name: 'HarvesterHotUnplug',
name: 'HarvesterHotUnplugModal',
emits: ['close'],
@@ -34,25 +35,8 @@ export default {
actionResource() {
return this.resources[0];
},
name() {
return this.modalData.name;
},
isVolume() {
return this.modalData.type === 'volume';
},
titleKey() {
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.title' : 'harvester.virtualMachine.hotUnplug.detachNIC.title';
},
actionLabelKey() {
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabel' : 'harvester.virtualMachine.hotUnplug.detachNIC.actionLabel';
},
successMessageKey() {
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.success' : 'harvester.virtualMachine.hotUnplug.detachNIC.success';
diskName() {
return this.modalData.diskName;
}
},
@@ -63,20 +47,14 @@ export default {
async save(buttonCb) {
try {
let res;
if (this.isVolume) {
res = await this.actionResource.doAction('removeVolume', { diskName: this.name });
} else {
res = await this.actionResource.doAction('removeNic', { interfaceName: this.name });
}
const res = await this.actionResource.doAction('removeVolume', { diskName: this.diskName });
if (res._status === 200 || res._status === 204) {
this.$store.dispatch(
'growl/success',
{
title: this.t('generic.notification.title.succeed'),
message: this.t(this.successMessageKey, { name: this.name })
message: this.t('harvester.modal.hotunplug.success', { name: this.diskName })
},
{ root: true }
);
@@ -86,14 +64,14 @@ export default {
} else {
const error = [res?.data] || exceptionToErrorsArray(res);
this.errors = error;
this['errors'] = error;
buttonCb(false);
}
} catch (err) {
const error = err?.data || err;
const message = exceptionToErrorsArray(error);
this.errors = message;
this['errors'] = message;
buttonCb(false);
}
}
@@ -109,7 +87,7 @@ export default {
>
<template #title>
<h4
v-clean-html="t(titleKey, { name })"
v-clean-html="t('harvester.virtualMachine.unplug.title', { name: diskName })"
class="text-default-text"
/>
<Banner
@@ -133,9 +111,9 @@ export default {
<AsyncButton
mode="apply"
:action-label="t(actionLabelKey)"
:waiting-label="t(actionLabelKey)"
:success-label="t(actionLabelKey)"
:action-label="t('harvester.virtualMachine.unplug.actionLabel')"
:waiting-label="t('harvester.virtualMachine.unplug.actionLabel')"
:success-label="t('harvester.virtualMachine.unplug.actionLabel')"
@click="save"
/>
</div>
@@ -154,8 +132,4 @@ export default {
justify-content: flex-end;
width: 100%;
}
::v-deep(.card-title) {
display: block;
}
</style>
@@ -1,133 +0,0 @@
<script>
import Tabbed from '@shell/components/Tabbed';
import Tab from '@shell/components/Tabbed/Tab';
import CruResource from '@shell/components/CruResource';
import { LabeledInput } from '@components/Form/LabeledInput';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import LabelValue from '@shell/components/LabelValue';
import CreateEditView from '@shell/mixins/create-edit-view';
export default {
name: 'HarvesterEditMIGConfiguration',
components: {
Tab,
Tabbed,
CruResource,
LabeledInput,
NameNsDescription,
LabelValue
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
const { profileSpec } = this.value.spec;
return { profileSpec: profileSpec || [] };
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
isView() {
return this.mode === 'view';
},
},
methods: {
updateBeforeSave() {
// MIGConfiguration CRD don't have any namespace field,
// so we need to remove the namespace field before saving
delete this.value.metadata.namespace;
// enable the MIGConfiguration when saving
this.value.spec.enabled = true;
},
labelTitle(profile) {
return `${ profile.name } (available : ${ this.available(profile) })`;
},
available(profile) {
const count = this.value.status?.profileStatus?.find((p) => p.id === profile.id)?.available;
return count || 0;
},
updateRequested(neu, profile) {
if (neu === null || neu === '') return;
const newValue = Number(neu);
const availableCount = this.available(profile);
if (newValue < 0) {
profile.requested = 0;
} else if ( newValue > availableCount ) {
profile.requested = availableCount;
} else {
profile.requested = newValue;
}
}
},
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
finish-button-mode="enable"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
:mode="mode"
/>
<Tabbed
v-bind="$attrs"
class="mt-15"
:side-tabs="true"
>
<Tab
name="profileSpec"
:label="t('harvester.migconfiguration.profileSpec')"
:weight="1"
class="bordered-table"
>
<div
v-for="(profile, index) in profileSpec"
:key="index"
>
<LabelValue
:value="labelTitle(profile)"
class="mb-10"
/>
<LabeledInput
v-model:value="profile.requested"
:min="0"
:disabled="isView"
type="number"
class="mb-20"
:label="`${t('harvester.migconfiguration.requested')}`"
@update:value="updateRequested($event, profile)"
/>
</div>
</Tab>
</Tabbed>
</CruResource>
</template>
@@ -8,7 +8,7 @@ import LabelValue from '@shell/components/LabelValue';
import { BadgeState } from '@components/BadgeState';
import { Banner } from '@components/Banner';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { RadioGroup } from '@components/Form/Radio';
import { RadioGroup, RadioButton } from '@components/Form/Radio';
import HarvesterDisk from '../../mixins/harvester-disk';
import Tags from '../../components/DiskTags';
import { HCI } from '../../types';
@@ -30,6 +30,7 @@ export default {
BadgeState,
Banner,
RadioGroup,
RadioButton,
ModalWithCard,
Tags,
},
@@ -183,11 +184,12 @@ export default {
},
forceFormattedDisabled() {
const lastFormattedAt = this.blockDevice?.status?.deviceStatus?.fileSystem?.LastFormattedAt;
const fileSystem = this.blockDevice?.status?.deviceStatus?.fileSystem.type;
const systems = ['ext4', 'XFS'];
if (this.blockDevice?.childParts?.length > 0) {
if (lastFormattedAt || this.blockDevice?.childParts?.length > 0) {
return true;
} else if (systems.includes(fileSystem)) {
return false;
@@ -444,7 +446,7 @@ export default {
/>
</div>
<div
v-if="(value.isNew && isLonghornV1) || isCorrupted"
v-if="(value.isNew && isLonghornV1 && !isFormatted) || isCorrupted"
class="col span-6"
>
<RadioGroup
@@ -457,6 +459,15 @@ export default {
:disabled="forceFormattedDisabled"
tooltip-key="harvester.host.disk.forceFormatted.toolTip"
>
<template #1="{option, listeners}">
<RadioButton
:label="option.label"
:val="option.value"
:value="value.forceFormatted"
:disabled="forceFormattedDisabled && !value.forceFormatted"
v-on="listeners"
/>
</template>
</RadioGroup>
</div>
<div
@@ -1,157 +0,0 @@
<script>
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
import { HCI } from '../../types';
import { DOC } from '../../config/doc-links';
export const hugepagesTHPEnabledMode = [{
label: 'Always',
value: 'always',
}, {
label: 'Madvise',
value: 'madvise',
}, {
label: 'Never',
value: 'never',
}];
export const hugepagesTHPShmemEnabledMode = [{
label: 'Always',
value: 'always',
}, {
label: 'Within Size',
value: 'within_size',
}, {
label: 'Advise',
value: 'advise',
}, {
label: 'Never',
value: 'never',
}, {
label: 'Deny',
value: 'deny',
}, {
label: 'Force',
value: 'force',
}];
export const hugepagesTHPDefragMode = [{
label: 'Always',
value: 'always',
}, {
label: 'Defer',
value: 'defer',
}, {
label: 'Defer+Madvise',
value: 'defer+madvise',
}, {
label: 'Madvise',
value: 'madvise',
}, {
label: 'Never',
value: 'never'
}];
export default {
name: 'HarvesterHugepages',
components: { LabeledSelect },
props: {
node: {
type: Object,
required: true,
},
registerBeforeHook: {
type: Function,
required: true,
},
},
computed: {
docsTransparentHugepagesLink() {
return DOC.TRANSPARENT_HUGEPAGES;
},
},
methods: {
async saveHugepages() {
this.hugepages['spec'] = this.spec;
await this.hugepages.save().catch((reason) => {
if (reason?.type === 'error') {
this.$store.dispatch('growl/error', {
title: this.t('harvester.notification.title.error'),
message: reason?.message,
}, { root: true });
return Promise.reject(new Error('saveHugepages error'));
}
});
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
this.hugepages = hash.find((node) => {
return node.id === this.node.id;
});
this.spec = this.hugepages.spec;
},
data() {
return {
hugepages: {},
spec: { transparent: {} },
hugepagesTHPEnabledMode,
hugepagesTHPShmemEnabledMode,
hugepagesTHPDefragMode,
};
},
created() {
this.registerBeforeHook(this.saveHugepages, 'saveHugepages');
},
};
</script>
<template>
<div>
<div>
<hr class="divider" />
<h3>
<t
k="harvester.host.hugepages.transparent.title"
:raw="true"
:url="docsTransparentHugepagesLink"
/>
</h3>
<div class="row mb-20">
<div class="col span-4">
<LabeledSelect
v-model:value="spec.transparent.enabled"
:label="t('harvester.host.hugepages.transparent.enabled')"
:options="hugepagesTHPEnabledMode"
/>
</div>
<div class="col span-4">
<LabeledSelect
v-model:value="spec.transparent.shmemEnabled"
:label="t('harvester.host.hugepages.transparent.shmemEnabled')"
:options="hugepagesTHPShmemEnabledMode"
/>
</div>
<div class="col span-4">
<LabeledSelect
v-model:value="spec.transparent.defrag"
:label="t('harvester.host.hugepages.transparent.defrag')"
:options="hugepagesTHPDefragMode"
/>
</div>
</div>
</div>
</div>
</template>
@@ -7,8 +7,6 @@ import { Checkbox } from '@components/Form/Checkbox';
import { HCI } from '../../types';
import { DOC } from '../../config/doc-links';
import { docLink } from '../../utils/feature-flags';
import isEqual from 'lodash/isEqual';
import { clone } from '@shell/utils/object';
export const ksmtunedMode = [{
value: 'standard',
@@ -66,13 +64,11 @@ export default {
this.enableMergeAcrossNodes = !!this.ksmtuned.spec?.mergeAcrossNodes;
this.spec = this.ksmtuned.spec;
this.originSpec = clone(this.ksmtuned.spec);
},
data() {
return {
ksmtuned: {},
originSpec: {},
spec: {},
thresCoef: 30,
ksmtunedMode,
@@ -103,11 +99,6 @@ export default {
methods: {
async saveKsmtuned() {
// no ksmtuned spec change, skip save
if (isEqual(this.spec, this.originSpec)) {
return;
}
this.spec.mergeAcrossNodes = this.enableMergeAcrossNodes ? 1 : 0;
this.ksmtuned['spec'] = this.spec;
@@ -28,7 +28,6 @@ import { HCI } from '../../types';
import HarvesterDisk from './HarvesterDisk';
import HarvesterSeeder from './HarvesterSeeder';
import HarvesterKsmtuned from './HarvesterKsmtuned';
import HarvesterHugepages from './HarvesterHugepages';
import Tags from '../../components/DiskTags';
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
import isEqual from 'lodash/isEqual';
@@ -51,7 +50,6 @@ export default {
ArrayListGrouped,
HarvesterDisk,
HarvesterKsmtuned,
HarvesterHugepages,
ButtonDropdown,
KeyValue,
Banner,
@@ -227,12 +225,6 @@ export default {
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
},
hasHugepagesSchema() {
const inStore = this.$store.getters['currentProduct'].inStore;
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.HUGEPAGES);
},
hasBlockDevicesSchema() {
const inStore = this.$store.getters['currentProduct'].inStore;
@@ -535,6 +527,7 @@ export default {
id: this.longhornNode.id,
opt: { force: true },
});
await new Promise((resolve) => setTimeout(resolve, '5000'));
await retrySave();
} else {
@@ -655,17 +648,6 @@ export default {
</template>
</ArrayListGrouped>
</Tab>
<Tab
v-if="hasHugepagesSchema"
name="Hugepages"
:weight="70"
:label="t('harvester.host.tabs.hugepages')"
>
<HarvesterHugepages
:node="value"
:register-before-hook="registerBeforeHook"
/>
</Tab>
<Tab
v-if="hasKsmtunedSchema"
name="Ksmtuned"
@@ -10,13 +10,9 @@ import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-anno
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../types';
import { NETWORK_TYPE, L2VLAN_MODE } from '../config/types';
import { removeObject } from '@shell/utils/array';
import { NETWORK_TYPE } from '../config/types';
const {
L2VLAN, UNTAGGED, OVERLAY, L2TRUNK_VLAN
} = NETWORK_TYPE;
const { ACCESS, TRUNK } = L2VLAN_MODE;
const { L2VLAN, UNTAGGED, OVERLAY } = NETWORK_TYPE;
const AUTO = 'auto';
const MANUAL = 'manual';
@@ -56,8 +52,6 @@ export default {
return {
config,
type,
l2VlanMode: this.value.vlanType === L2TRUNK_VLAN ? TRUNK : ACCESS,
vlanTrunk: this.parseVlanTrunk(config),
layer3Network: {
mode: layer3Network.mode || AUTO,
serverIPAddr: layer3Network.serverIPAddr || '',
@@ -114,10 +108,6 @@ export default {
}];
},
l2VlanTrunkModeFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('l2VlanTrunkMode');
},
kubeovnVpcSubnetSupport() {
return this.$store.getters['harvester-common/getFeatureEnabled']('kubeovnVpcSubnet');
},
@@ -141,16 +131,6 @@ export default {
});
},
l2VlanModeOptions() {
return [{
label: this.t('harvester.vlanStatus.vlanConfig.l2VlanMode.access'),
value: ACCESS,
}, {
label: this.t('harvester.vlanStatus.vlanConfig.l2VlanMode.trunk'),
value: TRUNK,
}];
},
networkTypes() {
const types = [L2VLAN, UNTAGGED];
@@ -169,22 +149,6 @@ export default {
return this.type === L2VLAN;
},
isL2VlanTrunkMode() {
if (this.isView) {
return this.value.vlanType === L2TRUNK_VLAN && this.l2VlanMode === TRUNK;
}
return this.type === L2TRUNK_VLAN || (this.l2VlanMode === TRUNK && this.type === L2VLAN);
},
isL2VlanAccessMode() {
if (this.isView) {
return this.value.vlanType === L2VLAN && this.l2VlanMode === ACCESS;
}
return this.type === L2VLAN && this.l2VlanMode === ACCESS;
},
isOverlayNetwork() {
if (this.isView) {
return this.value.vlanType === OVERLAY;
@@ -204,11 +168,11 @@ export default {
watch: {
type(newType) {
if (newType === OVERLAY) { // overlay network configuration
if (newType === OVERLAY) {
this.config.type = 'kube-ovn';
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
this.config.server_socket = '/run/openvswitch/kube-ovn-daemon.sock';
} else { // l2vlan or untagged network configuration
} else {
this.config.type = 'bridge';
this.config.promiscMode = true;
this.config.ipam = {};
@@ -216,20 +180,7 @@ export default {
delete this.config.provider;
delete this.config.server_socket;
}
},
l2VlanMode(newAccessMode) {
if (this.type !== L2VLAN) {
return;
}
if (newAccessMode === TRUNK) { // trunk mode
this.config.vlan = 0;
this.config.vlanTrunk = this.vlanTrunk;
} else { // access mode
delete this.config.vlanTrunk;
this.config.vlan = '';
}
},
}
},
methods: {
@@ -237,15 +188,7 @@ export default {
const errors = [];
if (this.isL2VlanNetwork || this.isUntaggedNetwork) {
if (this.isL2VlanTrunkMode && this.vlanTrunk.some((trunk) => trunk.minID === '')) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.vlanStatus.vlanConfig.vlanTrunk.minId') }));
}
if (this.isL2VlanTrunkMode && this.vlanTrunk.some((trunk) => trunk.maxID === '')) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.vlanStatus.vlanConfig.vlanTrunk.maxId') }));
}
if (this.isL2VlanAccessMode && !this.config.vlan && !this.isUntaggedNetwork) {
if (!this.config.vlan && !this.isUntaggedNetwork) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
}
@@ -274,30 +217,6 @@ export default {
await this.save(buttonCb);
},
parseVlanTrunk(config) {
if (config?.vlanTrunk && config?.vlanTrunk?.length > 0) {
return config.vlanTrunk;
}
return [{ minID: '', maxID: '' }];
},
removeVlanTrunk(trunk) {
removeObject(this.vlanTrunk, trunk);
},
addVlanTrunk() {
if (!this.config.vlanTrunk) {
this.config.vlanTrunk = [];
}
this.vlanTrunk.push({ minID: this.minId, maxID: this.maxId });
this.config.vlanTrunk = this.vlanTrunk;
},
vlanTrunkChange() {
this.config.vlanTrunk = this.vlanTrunk;
},
input(neu) {
if (neu === '') {
this.config.vlan = '';
@@ -373,76 +292,8 @@ export default {
required
/>
<LabeledSelect
v-if="isL2VlanNetwork && l2VlanTrunkModeFeatureEnabled"
v-model:value="l2VlanMode"
class="mb-20"
:options="l2VlanModeOptions"
:mode="mode"
:disabled="isEdit"
:label="t('harvester.vlanStatus.vlanConfig.l2VlanMode.label')"
required
/>
<div v-if="isL2VlanTrunkMode && l2VlanTrunkModeFeatureEnabled">
<div
v-for="(trunk, i) in vlanTrunk"
:key="i"
>
<div class="row mt-10">
<div class="col trunk-span">
<LabeledInput
v-model:value.number="trunk.minID"
class="mb-20"
required
placeholder="e.g. 1-4094"
:min="1"
:max="4094"
type="number"
:label="t('harvester.vlanStatus.vlanConfig.vlanTrunk.minId')"
:mode="mode"
@update:value="vlanTrunkChange"
/>
</div>
<div class="col trunk-span">
<LabeledInput
v-model:value.number="trunk.maxID"
class="mb-20"
:max="4094"
:min="1"
placeholder="e.g. 1-4094"
required
type="number"
:label="t('harvester.vlanStatus.vlanConfig.vlanTrunk.maxId')"
:mode="mode"
@update:value="vlanTrunkChange"
/>
</div>
<div class="col remove-btn mb-20">
<button
type="button"
:disabled="isView || vlanTrunk.length <= 1"
:aria-label="t('generic.ariaLabel.remove', {index: i+1})"
role="button"
class="btn role-link"
@click="removeVlanTrunk(trunk)"
>
{{ t('harvester.fields.remove') }}
</button>
</div>
</div>
</div>
<button
v-if="isL2VlanTrunkMode"
type="button"
class="btn btn-sm bg-primary mb-20"
:disabled="isView"
@click="addVlanTrunk"
>
{{ t('harvester.vlanStatus.vlanConfig.vlanTrunk.add') }}
</button>
</div>
<LabeledInput
v-if="isL2VlanAccessMode"
v-if="isL2VlanNetwork"
v-model:value.number="config.vlan"
class="mb-20"
required
@@ -465,7 +316,7 @@ export default {
/>
</Tab>
<Tab
v-if="isL2VlanAccessMode"
v-if="isL2VlanNetwork"
name="layer3Network"
:label="t('harvester.network.tabs.layer3Network')"
:weight="98"
@@ -524,12 +375,3 @@ export default {
</Tabbed>
</CruResource>
</template>
<style lang="scss" scoped>
.remove-btn {
align-self: center;
}
.trunk-span{
flex: 5;
}
</style>
@@ -15,7 +15,6 @@ import { HCI } from '../types';
import { sortBy } from '@shell/utils/sort';
import { BACKUP_TYPE } from '../config/types';
import { _EDIT, _CREATE } from '@shell/config/query-params';
import { isBackupTargetSettingEmpty, isBackupTargetSettingUnavailable } from '../utils/setting';
export default {
name: 'CreateVMSchedule',
@@ -94,7 +93,7 @@ export default {
return this.settings.find( (O) => O.id === 'backup-target');
},
isEmptyValue() {
return isBackupTargetSettingEmpty(this.backupTargetResource);
return this.getBackupTargetValueIsEmpty(this.backupTargetResource);
},
canUpdate() {
return this?.backupTargetResource?.canUpdate;
@@ -110,7 +109,7 @@ export default {
!!this.value.spec.maxFailure;
},
isBackupTargetUnAvailable() {
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && isBackupTargetSettingUnavailable(this.backupTargetResource);
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && (this.errorMessage || this.isEmptyValue) && this.canUpdate;
},
vmOptions() {
const nsVmList = this.$store.getters['harvester/all'](HCI.VM).filter((vm) => vm.metadata.namespace === this.value.metadata.namespace);
@@ -167,6 +166,20 @@ export default {
onTypeChange(newType) {
this.value.metadata.name = `svm${ newType }-${ this.value.spec.vmbackup.source.name }`;
},
getBackupTargetValueIsEmpty(resource) {
let out = true;
if (resource?.value) {
try {
const valueJson = JSON.parse(resource?.value);
out = !valueJson.type;
} catch (e) {}
}
return out;
},
validateFailure(count) {
if (this.value.spec.retain && count > this.value.spec.retain) {
this.value.spec['maxFailure'] = this.value.spec.retain;
@@ -296,9 +296,4 @@ export default {
:deep() .edit-help code {
padding: 1px 5px;
}
::v-deep(.banner__content.closable) {
white-space: normal;
word-break: break-word;
}
</style>
+3 -26
View File
@@ -7,7 +7,6 @@ import ResourceTabs from '@shell/components/form/ResourceTabs';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import Conditions from '@shell/components/form/Conditions';
import { Banner } from '@components/Banner';
import { allHash } from '@shell/utils/promise';
import { get } from '@shell/utils/object';
@@ -41,7 +40,6 @@ export default {
LabeledSelect,
LabeledInput,
NameNsDescription,
Conditions
},
mixins: [CreateEditView],
@@ -269,19 +267,7 @@ export default {
rebuildStatus() {
return this.value.longhornEngine?.status?.rebuildStatus;
},
isLHV2VolExpansionFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
},
isResizeDisabled() {
return (
!this.isLHV2VolExpansionFeatureEnabled &&
this.value?.isLonghornV2 &&
this.isEdit
);
},
}
},
watch: {
@@ -477,7 +463,7 @@ export default {
:output-modifier="true"
:increment="1024"
:mode="mode"
:disabled="isResizeDisabled"
:disabled="value?.isLonghornV2 && isEdit"
required
class="mb-20"
:suffix="GIBIBYTE"
@@ -485,7 +471,7 @@ export default {
/>
<Banner
v-if="isResizeDisabled"
v-if="value?.isLonghornV2 && isEdit"
color="warning"
>
<span>{{ t('harvester.volume.longhorn.disableResize') }}</span>
@@ -595,15 +581,6 @@ export default {
:label="t('nameNsDescription.name.label')"
/>
</Tab>
<Tab
v-if="!isCreate"
name="conditions"
:label="t('harvester.volume.tabs.conditions')"
class="bordered-table"
:mode="mode"
>
<Conditions :value="value" />
</Tab>
</ResourceTabs>
</CruResource>
</template>
@@ -1,196 +0,0 @@
<script>
import debounce from 'lodash/debounce';
import { _EDIT, _VIEW } from '@shell/config/query-params';
import { removeAt } from '@shell/utils/array';
import { Banner } from '@components/Banner';
import InfoBox from '@shell/components/InfoBox';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
export default {
name: 'AccessControlList',
emits: ['update:value'],
components: {
Banner, InfoBox, LabeledSelect, LabeledInput
},
props: {
value: {
type: Array,
default: null,
},
mode: {
type: String,
default: _EDIT,
},
},
data() {
const rows = (this.value || []).map((row) => {
return {
action: row.action || '',
direction: row.direction || '',
priority: row.priority || 0,
match: row.match || '',
};
});
return { rows };
},
computed: {
isView() {
return this.mode === _VIEW;
},
actionOptions() {
return [
{ label: 'Allow', value: 'allow' },
{ label: 'Drop', value: 'drop' },
{ label: 'Pass', value: 'pass' },
{ label: 'Reject', value: 'reject' },
{ label: 'Allow-related', value: 'allow-related' },
{ label: 'Allow-stateless', value: 'allow-stateless' },
];
},
directionOptions() {
return [
{ label: 'To-lport', value: 'to-lport' },
{ label: 'From-lport', value: 'from-lport' },
];
}
},
created() {
this.queueUpdate = debounce(this.update, 100);
},
methods: {
add() {
this.rows.push({
action: '',
direction: '',
priority: this.rows.length,
match: '',
});
this.queueUpdate();
},
removeRule(idx) {
removeAt(this.rows, idx);
this.queueUpdate();
},
update() {
if (this.isView) {
return;
}
this.$emit('update:value', this.rows);
}
},
};
</script>
<template>
<div>
<Banner
v-if="rows.length > 0"
color="info"
>
<t
k="harvester.subnet.acl.banner"
:raw="true"
/>
</Banner>
<div
v-for="(row, idx) in rows"
:key="idx"
>
<InfoBox class="box">
<button
type="button"
class="role-link btn btn-sm removeBtn"
:disabled="isView"
@click="removeRule(idx)"
>
<i class="icon icon-x" />
</button>
<div class="row">
<div class="col span-4">
<LabeledSelect
v-model:value="row.action"
:mode="mode"
class="mt-5 ml-5"
:options="actionOptions"
:required="true"
:label="t('harvester.subnet.acl.action.label')"
:placeholder="t('harvester.subnet.acl.action.placeholder')"
@update:value="update"
/>
</div>
<div class="col span-4">
<LabeledSelect
v-model:value="row.direction"
:mode="mode"
:options="directionOptions"
class="mt-5"
:required="true"
:label="t('harvester.subnet.acl.direction.label')"
:placeholder="t('harvester.subnet.acl.direction.placeholder')"
@update:value="update"
/>
</div>
<div class="col span-3">
<LabeledInput
v-model:value.number="row.priority"
type="number"
class="mb-20 mt-5"
:max="32767"
:min="0"
:mode="mode"
required
label-key="harvester.subnet.acl.priority.label"
@update:value="update"
/>
</div>
</div>
<div class="row">
<div class="col span-11">
<LabeledInput
v-model:value="row.match"
class="mb-5 ml-5"
:mode="mode"
required
:placeholder="t('harvester.subnet.acl.match.placeholder')"
label-key="harvester.subnet.acl.match.label"
@update:value="update"
/>
</div>
</div>
</InfoBox>
</div>
<button
type="button"
class="btn role-tertiary add"
:disabled="isView"
@click="add()"
>
<t k="harvester.subnet.acl.addRule" />
</button>
</div>
</template>
<style lang="scss" scoped>
.box {
position: relative;
}
.removeBtn {
position: absolute;
top: 10px;
right: 10px;
padding: 0px;
}
</style>
+3 -79
View File
@@ -14,8 +14,6 @@ import ArrayList from '@shell/components/form/ArrayList';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../../types';
import ResourceTabs from '@shell/components/form/ResourceTabs/index';
import { Banner } from '@components/Banner';
import AccessControlList from './AccessControlList';
export default {
name: 'EditSubnet',
@@ -23,7 +21,6 @@ export default {
emits: ['update:value'],
components: {
Banner,
CruResource,
LabeledInput,
LabeledSelect,
@@ -33,7 +30,6 @@ export default {
ArrayList,
ResourceTabs,
Loading,
AccessControlList
},
mixins: [CreateEditView],
@@ -42,9 +38,7 @@ export default {
created() {
const vpc = this.$route.query.vpc || '';
const enableDHCP = this.value?.spec?.enableDHCP || false;
set(this.value.spec, 'enableDHCP', enableDHCP);
set(this.value, 'spec', this.value.spec || {
cidrBlock: '',
protocol: NETWORK_PROTOCOL.IPv4,
@@ -52,9 +46,7 @@ export default {
vpc,
gatewayIP: '',
excludeIps: [],
private: false,
enableDHCP,
acls: []
private: false
});
},
@@ -89,6 +81,7 @@ export default {
protocolOptions() {
return Object.values(NETWORK_PROTOCOL);
},
provider: {
get() {
const raw = this.value.spec.provider;
@@ -132,21 +125,10 @@ export default {
}
},
watch: {
'value.spec.enableDHCP': {
handler(newValue) {
if (newValue === false) {
this.value.spec.dhcpV4Options = '';
this.value.spec.dhcpV6Options = '';
}
},
}
},
methods: {
async saveSubnet(buttonCb) {
const errors = [];
const name = this.value?.metadata?.name;
const hasEmptyAcls = this.value?.spec?.acls?.some((acl) => !acl.match || !acl.action || acl.priority === undefined || acl.priority === null);
try {
if (!name) {
@@ -157,8 +139,6 @@ export default {
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.provider.label') }, true));
} else if (this.value.spec.excludeIps.includes('')) {
errors.push(this.t('harvester.validation.subnet.excludeIps'));
} else if (hasEmptyAcls) {
errors.push(this.t('harvester.validation.subnet.aclEmptyError'));
}
if (errors.length > 0) {
@@ -265,45 +245,6 @@ export default {
/>
</div>
</div>
<div class="row mt-20">
<div class="col span-6">
<RadioGroup
v-model:value="value.spec.enableDHCP"
name="enabled"
:options="[true, false]"
:label="t('harvester.subnet.dhcp.label')"
:labels="[t('generic.enabled'), t('generic.disabled')]"
:mode="mode"
:tooltip="t('harvester.subnet.dhcp.tooltip')"
/>
<LabeledInput
v-if="value.spec.enableDHCP && value.spec.protocol === 'IPv4'"
v-model:value="value.spec.dhcpV4Options"
class="mb-20 mt-20"
:placeholder="t('harvester.subnet.dhcp.placeholder')"
:label="t('harvester.subnet.dhcp.v4Options')"
:mode="mode"
/>
<LabeledInput
v-if="value.spec.enableDHCP && value.spec.protocol === 'IPv6'"
v-model:value="value.spec.dhcpV6Options"
:placeholder="t('harvester.subnet.dhcp.placeholder')"
class="mb-20 mt-20"
:label="t('harvester.subnet.dhcp.v6Options')"
:mode="mode"
/>
<Banner
v-if="value.spec.enableDHCP"
color="info"
class="dhcpOption-banner"
>
<t
k="harvester.subnet.dhcp.dhcpOptionBanner"
:raw="true"
/>
</Banner>
</div>
</div>
<div class="row mt-20">
<div class="col span-6">
<RadioGroup
@@ -371,29 +312,12 @@ export default {
<div class="key">
<input
v-model="scope.row.value"
:placeholder="t('harvester.subnet.excludeIPs.placeholder')"
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
/>
</div>
</template>
</ArrayList>
</Tab>
<Tab
name="ACL"
:label="t('harvester.subnet.acl.label')"
:weight="-2"
class="bordered-table"
>
<AccessControlList
v-model:value="value.spec.acls"
:mode="mode"
/>
</Tab>
</ResourceTabs>
</CruResource>
</template>
<style lang="scss" scoped>
.dhcpOption-banner {
width: max-content;
}
</style>
@@ -2,17 +2,12 @@
import debounce from 'lodash/debounce';
import { _EDIT, _VIEW } from '@shell/config/query-params';
import { removeAt } from '@shell/utils/array';
import { Banner } from '@components/Banner';
import { DOC } from '../../config/doc-links';
import { docLink } from '../../utils/feature-flags';
export default {
name: 'StaticRoutes',
emits: ['update:value'],
components: { Banner },
props: {
value: {
type: Array,
@@ -50,12 +45,6 @@ export default {
nextHopIPTooltip() {
return this.t('harvester.vpc.staticRoutes.nextHopIP.tooltip');
},
vpcPeeringExamplesLink() {
const version = this.$store.getters['harvester-common/getServerVersion']();
return docLink(DOC.VPC_CONFIGURATION_EXAMPLES, version);
},
},
created() {
@@ -89,13 +78,6 @@ export default {
<template>
<div>
<Banner color="info">
<t
k="harvester.vpc.vpcPeerings.infoBanner"
:raw="true"
:url="vpcPeeringExamplesLink"
/>
</Banner>
<div
v-if="rows.length"
class="static-route-row"
@@ -1,5 +1,6 @@
<script>
import InfoBox from '@shell/components/InfoBox';
import { NETWORK_ATTACHMENT } from '@shell/config/types';
import { sortBy } from '@shell/utils/sort';
import { clone } from '@shell/utils/object';
@@ -13,13 +14,6 @@ export default {
components: { InfoBox, Base },
props: {
vm: {
type: Object,
default: () => {
return {};
}
},
mode: {
type: String,
default: 'create'
@@ -38,15 +32,10 @@ export default {
}
},
async fetch() {
await this.fetchHotunplugData();
},
data() {
return {
nameIdx: 1,
rows: this.addKeyId(clone(this.value)),
hotunpluggableNics: new Set(),
rows: this.addKeyId(clone(this.value)),
nameIdx: 1
};
},
@@ -75,57 +64,15 @@ export default {
return out;
},
canCheckHotunplug() {
return !!this.vm?.actions?.findHotunpluggableNics;
},
vmState() {
return this.vm?.stateDisplay;
}
},
watch: {
value(neu) {
this.rows = this.mergeHotplugData(clone(neu));
this.rows = neu;
},
vmState(newState, oldState) {
if (newState !== oldState) {
this.fetchHotunplugData();
}
}
},
methods: {
async fetchHotunplugData() {
if (!this.canCheckHotunplug) {
this.rows = this.mergeHotplugData(clone(this.value));
return;
}
try {
const resp = await this.vm.doAction('findHotunpluggableNics');
this.hotunpluggableNics = new Set(resp?.interfaces || []);
} catch (e) {
// eslint-disable-next-line no-console
console.error('Failed to fetch hot-unpluggable NICs:', e);
this.hotunpluggableNics = new Set();
}
this.rows = this.mergeHotplugData(clone(this.value));
},
mergeHotplugData(networks) {
return (networks || []).map((network) => ({
...network,
isHotunpluggable: this.hotunpluggableNics.has(network.name),
rowKeyId: network.rowKeyId || randomStr(10)
}));
},
add(type) {
const name = this.generateName();
@@ -171,11 +118,7 @@ export default {
update() {
this.$emit('update:value', this.rows);
},
unplugNIC(network) {
this.vm.unplugNIC(network.name);
},
}
}
};
</script>
@@ -186,28 +129,17 @@ export default {
v-for="(row, i) in rows"
:key="i"
>
<div class="box-title mb-10">
<h3>
{{ t('harvester.virtualMachine.network.title') }}
</h3>
<button
v-if="!isView"
type="button"
class="role-link btn btn-sm remove"
@click="remove(row)"
>
<i class="icon icon-x" />
</button>
<button
v-if="vm.hotplugNicFeatureEnabled && row.isHotunpluggable && isView"
type="button"
class="role-link btn btn-sm remove"
:disabled="!canCheckHotunplug"
@click="unplugNIC(row)"
>
{{ t('harvester.virtualMachine.hotUnplug.detachNIC.actionLabel') }}
</button>
</div>
<button
v-if="!isView"
type="button"
class="role-link remove-vol"
@click="remove(row)"
>
<i class="icon icon-x" />
</button>
<h3> {{ t('harvester.virtualMachine.network.title') }} </h3>
<Base
v-model:value="rows[i]"
:rows="rows"
@@ -230,13 +162,16 @@ export default {
</template>
<style lang='scss' scoped>
.box-title{
display: flex;
justify-content: space-between;
align-items: center;
.infoBox{
position: relative;
}
h3 {
margin-bottom: 0;
}
.remove-vol {
position: absolute;
top: 10px;
right: 16px;
padding:0px;
max-height: 28px;
min-height: 28px;
}
</style>
@@ -136,10 +136,6 @@ export default {
pvcs() {
return this.$store.getters['harvester/all'](PVC) || [];
},
isLHV2VolExpansionFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
},
},
watch: {
@@ -274,8 +270,8 @@ export default {
},
isResizeDisabled(volume) {
if (this.isLHV2VolExpansionFeatureEnabled) return false;
if (this.isCreate || volume.newCreateId) return false;
if (this.isCreate) return false;
if (volume.newCreateId) return false;
const isStopped = this.vm.stateDisplay === OFF;
const isLonghornV2 = this.isLonghornV2(volume);
@@ -303,57 +299,55 @@ export default {
v-for="(volume, i) in rows"
:key="volume.id"
>
<InfoBox>
<div class="box-title mb-10">
<h3>
<span
v-if="volume.to && isVirtualType"
class="title"
<InfoBox class="box">
<button
v-if="!isView"
type="button"
class="role-link btn btn-sm remove"
@click="removeVolume(volume)"
>
<i class="icon icon-x" />
</button>
<button
v-if="volume.hotpluggable && isView"
type="button"
class="role-link btn remove"
@click="unplugVolume(volume)"
>
{{ t('harvester.virtualMachine.unplug.detachVolume') }}
</button>
<h3>
<span
v-if="volume.to && isVirtualType"
class="title"
>
<router-link :to="volume.to">
{{ t('harvester.virtualMachine.volume.edit') }} {{ headerFor(volume.source) }}
</router-link>
<BadgeStateFormatter
v-if="volume.pvc"
class="ml-10 state"
:arbitrary="true"
:row="volume.pvc"
:value="volume.pvc.state"
/>
<a
v-if="dev && !!volume.pvc && !!volume.pvc.resourceExternalLink"
v-clean-tooltip="t(volume.pvc.resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
class="ml-5 resource-external"
rel="nofollow noopener noreferrer"
target="_blank"
:href="volume.pvc.resourceExternalLink.url"
>
<router-link :to="volume.to">
{{ t('harvester.virtualMachine.volume.edit') }} {{ headerFor(volume.source) }}
</router-link>
<i class="icon icon-external-link" />
</a>
</span>
<BadgeStateFormatter
v-if="volume.pvc"
class="ml-10 state"
:arbitrary="true"
:row="volume.pvc"
:value="volume.pvc.state"
/>
<a
v-if="dev && !!volume.pvc && !!volume.pvc.resourceExternalLink"
v-clean-tooltip="t(volume.pvc.resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
class="ml-5 resource-external"
rel="nofollow noopener noreferrer"
target="_blank"
:href="volume.pvc.resourceExternalLink.url"
>
<i class="icon icon-external-link" />
</a>
</span>
<span v-else>
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
</span>
</h3>
<button
v-if="!isView"
type="button"
class="role-link btn btn-sm remove"
@click="removeVolume(volume)"
>
<i class="icon icon-x" />
</button>
<button
v-if="volume.hotpluggable && isView"
type="button"
class="role-link btn btn-sm remove"
@click="unplugVolume(volume)"
>
{{ t('harvester.virtualMachine.hotUnplug.detachVolume.actionLabel') }}
</button>
</div>
<span v-else>
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
</span>
</h3>
<div>
<component
:is="componentFor(volume.source)"
@@ -412,7 +406,7 @@ export default {
:label="ucFirst(value.volumeBackups.error.message)"
/>
<Banner
v-if="!isLHV2VolExpansionFeatureEnabled && isLonghornV2(volume) && !isView"
v-if="isLonghornV2(volume) && !isView"
color="warning"
:label="t('harvester.volume.longhorn.disableResize')"
/>
@@ -497,24 +491,25 @@ export default {
</template>
<style lang='scss' scoped>
.box-title {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
margin-bottom: 0;
}
.box {
position: relative;
}
.title {
display: flex;
align-items: center;
.state {
font-size: 16px;
}
}
.remove {
position: absolute;
top: 10px;
right: 10px;
padding: 0px;
}
.bootOrder {
display: flex;
align-items: center;
@@ -99,6 +99,7 @@ export default {
this.allPVCs
.filter( (pvc) => {
let isAvailable = true;
let isBeingUsed = false;
this.rows.forEach( (O) => {
if (O.volumeName === pvc.metadata.name) {
@@ -110,16 +111,17 @@ export default {
return false;
}
// already used as image volume
if (this.idx > 0 && pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
return false;
}
if (pvc.isGoldenImageVolume) {
return false;
}
return isAvailable && pvc.isAvailable;
if (pvc.attachVM && isAvailable && pvc.attachVM?.id === this.vm?.id && this.isEdit) {
isBeingUsed = false;
} else if (pvc.attachVM) {
isBeingUsed = true;
}
return isAvailable && !isBeingUsed && pvc.isAvailable;
})
.map((pvc) => {
return {
@@ -2,7 +2,7 @@
import { isEqual } from 'lodash';
import { mapGetters } from 'vuex';
import Tabbed from '@shell/components/Tabbed';
import { clone } from '@shell/utils/object';
import { clone, set } from '@shell/utils/object';
import Tab from '@shell/components/Tabbed/Tab';
import { Checkbox } from '@components/Form/Checkbox';
import CruResource from '@shell/components/CruResource';
@@ -434,44 +434,38 @@ export default {
}
},
_compareDisksIgnoreStorage(disksA = [], disksB = []) {
if (disksA.length !== disksB.length) return false;
restartVM() {
if (this.mode !== 'edit') {
return;
}
if (!this.value.isRunning) {
return;
}
const cloneDeepNewVM = clone(this.value);
const sortedA = [...disksA].sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
const sortedB = [...disksB].sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
// new VM
delete cloneDeepNewVM?.metadata;
delete cloneDeepNewVM?.__clone;
for (let i = 0; i < sortedA.length; i++) {
const cloneA = clone(sortedA[i]);
const cloneB = clone(sortedB[i]);
// old VM
delete this.cloneVM?.metadata;
delete this.cloneVM?.__clone;
// remove the storage field before comparison
delete cloneA?.spec?.resources?.requests?.storage;
delete cloneB?.spec?.resources?.requests?.storage;
// add empty hostDevices to old VM as CRD does not have it.
const devicesObj = this.cloneVM?.spec?.template?.spec?.domain?.devices;
if (!isEqual(cloneA, cloneB)) return false;
if (devicesObj && devicesObj.hostDevices === undefined) {
set(devicesObj, 'hostDevices', []);
}
return true;
},
const oldVM = JSON.parse(JSON.stringify(this.cloneVM));
const newVM = JSON.parse(JSON.stringify(cloneDeepNewVM));
_shouldPromptRestart(oldVM, newVM) {
const specChanged = !isEqual(oldVM?.spec, newVM?.spec);
// check if the disk defined in annotations has changed, ignoring storage size
const oldDisks = parseVolumeClaimTemplates(oldVM);
const newDisks = parseVolumeClaimTemplates(newVM);
const diskChanged = !this._compareDisksIgnoreStorage(oldDisks, newDisks);
return specChanged || diskChanged;
},
restartVM() {
if (this.mode !== 'edit' || !this.value.isRunning) return;
const needsRestart = this._shouldPromptRestart(this.cloneVM, this.value);
// if no restart is needed (either no changes or only size changed)
if (!needsRestart) return;
// we won't show restart dialog in yaml page as we don't have a way to detect change in yaml editor.
// only check VM is changed in form page.
if (isEqual(oldVM, newVM)) {
return;
}
return new Promise((resolve) => {
this.isOpen = true;
@@ -490,7 +484,6 @@ export default {
if (this.isSingle) {
if (!this.value.spec.template.spec.hostname) {
this.value.spec.template.spec['hostname'] = this.value.metadata.name;
this.spec.template.spec['hostname'] = this.value.metadata.name;
}
}
@@ -899,10 +892,7 @@ export default {
/>
</div>
<div
v-if="!value.vmMachineTypeAutoFeatureEnabled"
class="col span-6"
>
<div class="col span-6">
<LabeledSelect
v-model:value="machineType"
label-key="harvester.virtualMachine.input.MachineType"
@@ -39,10 +39,6 @@ export default {
return this.mode === _VIEW;
},
disableAdd() {
return this.isStandaloneHarvester && this.rows.some((row) => row.namespace === '*');
},
showAdd() {
return !this.isView;
},
@@ -145,7 +141,6 @@ export default {
>
<button
type="button"
:disabled="disableAdd"
class="btn role-tertiary add"
@click="add()"
>
@@ -66,26 +66,17 @@ export default {
},
namespaceOptions() {
const namespaces = this.filteredNamespaces;
const selected = this.rows.map((r) => r?.namespace);
if (this.isStandaloneHarvester) {
return [
{ label: this.t('generic.all'), value: '*' },
...namespaces.map((ns) => ({
label: ns.metadata.name,
value: ns.id,
disabled: selected.includes(ns.id) && ns.id !== this.row.namespace
}))
];
}
const out = (this.filteredNamespaces || []).map((namespace) => {
return {
label: namespace.metadata.name,
value: namespace.id,
};
});
return [{
label: this.t('generic.all'),
value: '*',
},
...namespaces.map((ns) => ({ label: ns.metadata.name, value: ns.id }))
];
}, ...out];
},
guestClusterOptions() {
@@ -1,42 +0,0 @@
<script>
import NetworkPolicy from '@shell/edit/networking.k8s.io.networkpolicy';
import { Banner } from '@components/Banner';
import { DOC } from '../config/doc-links';
import { docLink } from '../utils/feature-flags';
export default {
components: {
NetworkPolicy,
Banner
},
computed: {
networkPolicyDocLink() {
const version = this.$store.getters['harvester-common/getServerVersion']();
return docLink(DOC.NETWORK_POLICY, version);
}
}
};
</script>
<template>
<div class="network-policy-edit">
<Banner color="info">
<t
k="harvester.networkPolicy.banner"
:url="networkPolicyDocLink"
:raw="true"
/>
</Banner>
<NetworkPolicy v-bind="$attrs" />
</div>
</template>
<style lang="scss" scoped>
.network-policy-edit {
flex: auto;
display: flex;
flex-direction: column;
}
</style>
+13 -125
View File
@@ -154,19 +154,15 @@ harvester:
nodeTimeout:
label: Node Collection Timeout
tooltip: Minutes allowed for collecting logs/configurations on nodes.<br/>See docs support-bundle-node-collection-timeout for detail.
hotplugVolume:
hotplug:
success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
title: Add Volume
hotplugNic:
success: 'The settings have been saved, but the network interface {interfaceName} will be attached only after the virtual machine is migrated.'
title: Add Network Interface
vmNetwork: Virtual Machine Network
macAddress: MAC Address
macAddressTooltip: If left blank, the MAC address will be automatically generated.
cpuMemoryHotplug:
success: 'CPU and Memory are updated to the virtual machine { vm }.'
title: Edit CPU and Memory
maxResourcesMessage: 'You can increase the CPU to maximum { maxCpu }C and memory to maximum { maxMemory }.'
hotunplug:
success: 'Volume { name } is detached successfully.'
snapshot:
title: Take Snapshot
name: Name
@@ -230,8 +226,7 @@ harvester:
disableCPUManager: Disable CPU Manager
cordon: Cordon
uncordon: Uncordon
addHotplugVolume: Add Volume
addHotplugNic: Hotplug Network Interface
addHotplug: Add Volume
exportImage: Export Image
viewlogs: View Logs
cancelExpand: Cancel Expand
@@ -241,7 +236,6 @@ harvester:
shutdown: Shut Down
powerOn: Power On
reboot: Reboot
reconnect: Reconnect
forceStop: Force Stop
formatters:
hardwareResourceGauge:
@@ -277,7 +271,6 @@ harvester:
cronExpression: Cron Expression
retain: Retain
scheduleType: Type
configuredProfiles: Configured Profiles
maxFailure: Max Failure
sourceVm: Source Virtual Machine
vmSchedule: Virtual Machine Schedule
@@ -410,7 +403,7 @@ harvester:
sha512: 'Invalid SHA512 checksum.'
subnet:
excludeIps: 'Exclude IPs cannot be empty. Please remove or fill in the exclude IPs.'
aclEmptyError: The fields in subnet access control list rule can not be empty.
dashboard:
label: Dashboard
header: "Harvester Cluster: {cluster}"
@@ -447,7 +440,6 @@ harvester:
storage: Storage
labels: Labels
ksmtuned: Ksmtuned
hugepages: Hugepages
seeder: Out-of-band Access
detail:
kvm:
@@ -520,20 +512,6 @@ harvester:
fullScans: Full Scans
stableNodeChains: Stable Node Chains
stableNodeDups: Stable Node Dups
hugepages:
meminfo: Meminfo
transparent:
title: Transparent Hugepages <a href="{url}" target="_blank"><i class="icon icon-info" /></a>
enabled: Enabled
shmemEnabled: Shared Memory Enabled
defrag: Defragmentation
status:
anon: Anonymous Hugepages (bytes)
size: Default Hugepage Size (bytes)
total: Total Hugepages
free: Free Hugepages
rsvd: Reserved Hugepages
surp: Surplus Hugepages
disk:
add: Add Disk
path:
@@ -602,16 +580,6 @@ harvester:
title: Enable CPU and memory hotplug
tooltip: The default maximum CPU and maximum memory are {hotPlugTimes} times based on CPU and memory.
restartVMMessage: Restart action is required for the virtual machine configuration change to take effect
hotUnplug:
actionLabel: Detach
detachVolume:
title: 'Are you sure that you want to detach volume {name}?'
actionLabel: Detach Volume
success: 'Volume { name } is detached successfully.'
detachNIC:
title: 'Are you sure that you want to detach network interface {name}?'
actionLabel: Detach Network Interface
success: 'The settings have been saved, but the network interface {name} will be detached only after the virtual machine is migrated.'
instance:
singleInstance:
multipleInstance:
@@ -643,6 +611,11 @@ harvester:
title: 'Select the volume you want to delete:'
deleteAll: Delete All
tips: "Warn: The snapshots of the virtual machine will be deleted with virtual machine and the snapshots of volume will be deleted with volume."
unplug:
title: 'Are you sure that you want to detach volume {name} ?'
actionLabel: Detach
detachVolume:
Detach Volume
restartTip: |-
{restart, select,
true {Restart}
@@ -863,7 +836,6 @@ harvester:
snapshots: Snapshots
datasource: Data Source
details: Details
conditions: Conditions
size: Size
volumeMode: Volume Mode
source: Source
@@ -1072,13 +1044,6 @@ harvester:
gateway:
label: Gateway IP
placeholder: e.g. 172.20.0.1
dhcp:
label: Dynamic Host Configuration Protocol (DHCP)
v4Options: DHCPV4Options
v6Options: DHCPV6Options
placeholder: key1=value1, key2=value2
dhcpOptionBanner: DHCP options is a key/value string concatenate with comma. For more detail, please refer to <a href="https://kubeovn.github.io/docs/v1.13.x/en/kubevirt/dhcp/" target="_blank">KubOVN document</a>
tooltip: Enable DHCP server for this subnet. When enabled, VMs can automatically obtain IP addresses from this subnet.
private:
label: Private Subnet
tooltip: Enable network isolation for this Subnet. When enabled, VMs can only communicate within this subnet, even if other subnets exist under the same VPC.
@@ -1089,25 +1054,6 @@ harvester:
placeholder: e.g. 172.16.0.0/16
excludeIPs:
tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list.
placeholder: single IP or 192.168.0.100..192.168.0.200
acl:
label: Access Control List
tooltip: The ACL to apply to this Subnet. Must be one of the ACLs in the same namespace.
action:
label: Action
placeholder: Please select an action
direction:
label: Direction
placeholder: Please select a direction
addRule: Add Rule
priority:
label: Priority
placeholder: Please select a priority
match:
label: Match
placeholder: e.g. ip4.dst == 10.10.0.2
banner: The supported field in ACL match can refer to <a href="https://kubeovn.github.io/docs/v1.14.x/en/guide/subnet/#subnet-acl" target="_blank">KubeOvn Subnet ACL document</a>
vpc:
noAddonEnabled:
@@ -1134,10 +1080,6 @@ harvester:
placeholder: e.g. 169.254.0.1/16
remoteVpc:
label: Remote VPC
infoBanner: The static route destination CIDR must cover all subnets CIDR from remote VPC Peer. Read <a href="{url}" target="_blank">VPC Peering Configuration Examples</a> for more information.
networkPolicy:
label: Network Policies
banner: The network policies must be used for VMs attached to overlay networks. Please read the <a href="{url}" target="_blank">harvester document</a> how the network policy works.
network:
label: Virtual Machine Networks
tabs:
@@ -1206,7 +1148,7 @@ harvester:
vlan: VLAN ID
exclude:
label: Exclude IPs
placeholder: CIDR format, e.g. 172.16.0.10/32
placeholder: e.g. 172.16.0.1
invalid: '"Exclude list" is invalid.'
addIp: Add Exclude IP
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
@@ -1296,21 +1238,6 @@ harvester:
ntpServers:
isNotIPV4: The address you entered is not IPv4 or host. Please enter a valid IPv4 address or a host address.
isDuplicate: There are duplicate NTP server configurations.
kubevirtMigration:
parseError: "Failed to parse configuration: {error}"
parallelMigrationsPerCluster: "Parallel Migrations Per Cluster"
parallelOutboundMigrationsPerNode: "Parallel Outbound Migrations Per Node"
bandwidthPerMigration: "Bandwidth Per Migration"
bandwidthPerMigrationTooltip: '0 is KubeVirt default, representing unlimited bandwidth'
completionTimeoutPerGiB: "Completion Timeout Per GiB"
progressTimeout: "Progress Timeout"
allowAutoConverge: "Allow Auto Converge"
allowPostCopy: "Allow Post Copy"
unsafeMigrationOverride: "Unsafe Migration Override"
allowWorkloadDisruption: "Allow Workload Disruption"
disableTLS: "Disable TLS"
matchSELinuxLevelOnMigration: "Match SELinux Level On Migration"
cloudTemplate:
label: Cloud Configuration Templates
templateType: Template Type
@@ -1526,14 +1453,6 @@ harvester:
vlanStatus:
vlanConfig:
label: Network Configuration
l2VlanMode:
access: Access
trunk: Trunk
label: Mode
vlanTrunk:
minId: Minimum VLAN ID
maxId: Maximum VLAN ID
add: Add VLAN Trunk
clusterNetwork:
title: Cluster Network Configuration
@@ -1707,26 +1626,6 @@ harvester:
middle: here
suffix: to enable it to manage your SR-IOV GPU devices.
migconfiguration:
label: vGPU MIG Configurations
infoBanner: To configure the MIG configuration, please disable it first and re-enable after editing the configuration.
profileSpec: Profile Specs
profileStatus: Profile Status
tableHeaders:
profileName: Profile Name
total: Total
vGPUID: vGPU ID
available: Available
requested: Requested
requested: Requested
available: Available
total: Total
vGPUID: vGPU ID
goSriovGPU:
prefix: Please enable the supported GPU devices in
middle: SR-IOV GPU Devices
suffix: page to manage the vGPU MIG configurations.
vgpu:
label: vGPU Devices
noPermission: Please contact system administrator to add Harvester add-ons first.
@@ -1819,8 +1718,9 @@ advancedSettings:
'harv-server-version': Harvester server version.
'harv-upgrade-checker-enabled': Specifies whether to enable Harvester upgrade check or not. Default is True.
'harv-upgrade-checker-url': Default Harvester upgrade check url. Only used when the <code>upgrade-checker-enabled</code> is equal to True.
'harv-ui-source': Configure how to load the UI source. The `External` option is used to load the UI from a ui-index URL, while the `Bundled` option loads the UI from the local Harvester server.
'harv-ui-source': Configure how to load the UI source.
'harv-ui-index': 'HTML index location for the UI.'
'harv-ui-plugin-index': 'JS index location for the Harvester plugin UI.'
'harv-cluster-registration-url': Registration URL for multi-cluster management.
'harv-http-proxy': 'HTTP proxy for Harvester to access external services.'
'harv-additional-ca': 'Custom CA root certificates for TLS validation.'
@@ -1845,7 +1745,6 @@ advancedSettings:
'harv-vm-migration-network': 'Segregated network for VM migration traffic.'
'harv-rancher-cluster': 'Configure Rancher cluster integration settings for guest cluster management.'
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
'harv-kubevirt-migration': 'Configure cluster-wide KubeVirt live migration parameters.'
typeLabel:
kubevirt.io.virtualmachine: |-
@@ -1908,11 +1807,6 @@ typeLabel:
one { Virtual Private Cloud }
other { Virtual Private Clouds }
}
networking.k8s.io.networkpolicy: |-
{count, plural,
one { Network Policy }
other { Network Policies }
}
harvesterhci.io.cloudtemplate: |-
{count, plural,
one { Cloud Configuration Template }
@@ -2001,12 +1895,6 @@ typeLabel:
other { vGPU Devices }
}
devices.harvesterhci.io.migconfiguration: |-
{count, plural,
one { vGPU MIG Configuration }
other { vGPU MIG Configurations }
}
harvesterhci.io.secret: |-
{count, plural,
one { Secret }
@@ -1,177 +0,0 @@
<script>
import { STATE, NAME } from '@shell/config/table-headers';
import { allHash } from '@shell/utils/promise';
import Banner from '@components/Banner/Banner.vue';
import Loading from '@shell/components/Loading';
import ResourceTable from '@shell/components/ResourceTable';
import { HCI } from '../types';
import { ADD_ONS } from '../config/harvester-map';
import MessageLink from '@shell/components/MessageLink';
export default {
name: 'ListMIGConfigurations',
inheritAttrs: false,
components: {
Banner,
Loading,
ResourceTable,
MessageLink,
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.schema = this.$store.getters[`${ inStore }/schemaFor`](HCI.MIG_CONFIGURATION);
this.hasAddonSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.ADD_ONS);
if (this.hasSchema) {
try {
const hash = await allHash({
migconfigs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.MIG_CONFIGURATION }),
vGpuDevices: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VGPU_DEVICE }),
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS })
});
this.hasPCIAddon = hash.addons.find((addon) => addon.name === ADD_ONS.PCI_DEVICE_CONTROLLER)?.spec?.enabled === true;
this.hasSriovgpuAddon = hash.addons.find((addon) => addon.name === ADD_ONS.NVIDIA_DRIVER_TOOLKIT_CONTROLLER)?.spec?.enabled === true;
this.hasSRIOVGPUSchema = !!this.$store.getters[`${ inStore }/schemaFor`](HCI.SR_IOVGPU_DEVICE);
if (this.hasSRIOVGPUSchema) {
await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOVGPU_DEVICE });
}
this.rows = hash.migconfigs;
} catch (e) {}
}
},
data() {
return {
rows: [],
schema: null,
hasAddonSchema: false,
hasPCIAddon: false,
hasSriovgpuAddon: false,
hasSRIOVGPUSchema: false,
toVGpuAddon: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.NVIDIA_DRIVER_TOOLKIT_CONTROLLER }?mode=edit`,
toPciAddon: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.PCI_DEVICE_CONTROLLER }?mode=edit`,
SRIOVGPUPage: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.NVIDIA_DRIVER_TOOLKIT_CONTROLLER }?mode=edit`,
};
},
computed: {
hasSchema() {
return !!this.schema;
},
rowsData() {
const inStore = this.$store.getters['currentProduct'].inStore;
const rows = this.$store.getters[`${ inStore }/all`](HCI.MIG_CONFIGURATION) || [];
return rows;
},
sriovGPUPage() {
return {
name: 'harvester-c-cluster-resource',
params: { cluster: this.$store.getters['clusterId'], resource: HCI.SR_IOVGPU_DEVICE },
};
},
showEnableSRIOVGPUMessage() {
return this.rowsData.length === 0;
},
headers() {
const cols = [
STATE,
NAME,
{
name: 'address',
label: 'Address',
value: 'spec.gpuAddress',
sort: ['spec.gpuAddress']
},
{
name: 'Configured Profile',
label: 'Configured Count',
labelKey: 'harvester.tableHeaders.configuredProfiles',
value: 'configuredProfiles',
sort: ['configuredProfiles'],
align: 'center'
},
{
name: 'status',
label: 'Status',
labelKey: 'tableHeaders.status',
sort: ['status.status'],
value: 'status.status',
},
];
return cols;
},
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else-if="!hasAddonSchema">
<Banner color="warning">
{{ t('harvester.vgpu.noPermission') }}
</Banner>
</div>
<div v-else-if="!hasSriovgpuAddon || !hasPCIAddon">
<Banner
v-if="!hasSriovgpuAddon"
color="warning"
>
<MessageLink
:to="toVGpuAddon"
prefix-label="harvester.vgpu.goSetting.prefix"
middle-label="harvester.vgpu.goSetting.middle"
suffix-label="harvester.vgpu.goSetting.suffix"
/>
</Banner>
<Banner
v-if="!hasPCIAddon"
color="warning"
>
<MessageLink
:to="toPciAddon"
prefix-label="harvester.pci.goSetting.prefix"
middle-label="harvester.pci.goSetting.middle"
suffix-label="harvester.pci.goSetting.suffix"
/>
</Banner>
</div>
<div v-else-if="hasSchema">
<Banner
v-if="showEnableSRIOVGPUMessage"
color="warning"
>
<MessageLink
:to="sriovGPUPage"
prefix-label="harvester.migconfiguration.goSriovGPU.prefix"
middle-label="harvester.migconfiguration.goSriovGPU.middle"
suffix-label="harvester.migconfiguration.goSriovGPU.suffix"
/>
</Banner>
<Banner
v-if="!showEnableSRIOVGPUMessage"
color="warning"
:label="t('harvester.migconfiguration.infoBanner')"
/>
<ResourceTable
v-bind="$attrs"
:groupable="false"
:namespaced="false"
:headers="headers"
:schema="schema"
:rows="rowsData"
key-field="_key"
/>
</div>
</template>
@@ -4,7 +4,7 @@ import Loading from '@shell/components/Loading';
import MessageLink from '@shell/components/MessageLink';
import Masthead from '@shell/components/ResourceList/Masthead';
import ResourceTable from '@shell/components/ResourceTable';
import { isBackupTargetSettingEmpty } from '../utils/setting';
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
import FilterVMSchedule from '../components/FilterVMSchedule';
import { HCI } from '../types';
@@ -39,7 +39,7 @@ export default {
this.settings = hash.settings;
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.SETTING)) {
const backupTargetResource = hash.settings.find( (O) => O.id === 'backup-target');
const isEmpty = isBackupTargetSettingEmpty(backupTargetResource);
const isEmpty = this.getBackupTargetValueIsEmpty(backupTargetResource);
if (backupTargetResource && !isEmpty) {
this.testConnect();
@@ -78,6 +78,20 @@ export default {
}
},
getBackupTargetValueIsEmpty(resource) {
let out = true;
if (resource?.value) {
try {
const valueJson = JSON.parse(resource?.value);
out = !valueJson.type;
} catch (e) {}
}
return out;
},
getRow(row) {
return row.status && row.status.source;
},
@@ -169,9 +183,11 @@ export default {
backupTargetResource() {
return this.settings.find((O) => O.id === 'backup-target');
},
isEmptyValue() {
return isBackupTargetSettingEmpty(this.backupTargetResource);
return this.getBackupTargetValueIsEmpty(this.backupTargetResource);
},
canUpdate() {
return this?.backupTargetResource?.canUpdate;
},
@@ -74,7 +74,7 @@ export default {
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const _hash = {
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM, opt: { force: true } }),
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
pod: this.$store.dispatch(`${ inStore }/findAll`, { type: POD }),
pvcs: this.$store.dispatch(`${ inStore }/findAll`, { type: PVC }),
pvs: this.$store.dispatch(`${ inStore }/findAll`, { type: PV }),
+19 -81
View File
@@ -299,11 +299,9 @@ export default {
async created() {
await this.$store.dispatch(`${ this.inStore }/findAll`, { type: SECRET });
if (!this.value.vmMachineTypeAutoFeatureEnabled) {
if (this.value.vmMachineTypesFeatureEnabled) {
try {
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl'](
'v1/harvester/clusters/local?link=machineTypes'
);
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/clusters/local?link=machineTypes');
const machineTypes = await this.$store.dispatch('harvester/request', { url });
this.machineTypes = machineTypes;
@@ -704,11 +702,21 @@ export default {
parseDiskRows(disk) {
const disks = [];
const volumes = [];
const diskNameLabels = [];
const diskNameLables = [];
const volumeClaimTemplates = [];
disk.forEach( (R, index) => {
const dataVolumeName = this.parseDataVolumeName(R);
const prefixName = this.value.metadata?.name || '';
let dataVolumeName = '';
if (R.source === SOURCE_TYPE.ATTACH_VOLUME) {
dataVolumeName = R.volumeName;
} else if (this.isClone || !this.hasCreateVolumes.includes(R.realName)) {
dataVolumeName = `${ prefixName }-${ R.name }-${ randomStr(5).toLowerCase() }`;
} else {
dataVolumeName = R.realName;
}
const _disk = this.parseDisk(R, index);
const _volume = this.parseVolume(R, dataVolumeName);
@@ -716,7 +724,7 @@ export default {
disks.push(_disk);
volumes.push(_volume);
diskNameLabels.push(dataVolumeName);
diskNameLables.push(dataVolumeName);
if (R.source !== SOURCE_TYPE.CONTAINER) {
volumeClaimTemplates.push(_dataVolumeTemplate);
@@ -757,9 +765,6 @@ export default {
}
}
const specDisks = this.spec?.template?.spec?.domain?.devices?.disks;
const mergedDisks = this.mergeDeviceList(specDisks, disks);
let spec = {
...this.spec,
runStrategy: this.runStrategy,
@@ -782,7 +787,7 @@ export default {
...this.spec.template?.spec?.domain,
devices: {
...this.spec.template?.spec?.domain?.devices,
disks: mergedDisks,
disks,
},
},
volumes,
@@ -881,16 +886,13 @@ export default {
interfaces.push(_interface);
});
const specInterfaces = this.spec?.template?.spec?.domain?.devices?.interfaces;
const mergedInterfaces = this.mergeInterfaceList(specInterfaces, interfaces);
const spec = {
...this.spec.template.spec,
domain: {
...this.spec.template.spec.domain,
devices: {
...this.spec.template.spec.domain.devices,
interfaces: mergedInterfaces,
interfaces,
},
},
networks
@@ -1003,25 +1005,6 @@ export default {
this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
},
parseDataVolumeName(R) {
const prefixName = this.value.metadata?.name || '';
let dataVolumeName = '';
if (!R.dataVolumeName) {
if (R.source === SOURCE_TYPE.ATTACH_VOLUME) {
dataVolumeName = R.volumeName;
} else if (this.isClone || !this.hasCreateVolumes.includes(R.realName)) {
dataVolumeName = `${ prefixName }-${ R.name }-${ randomStr(5).toLowerCase() }`;
} else {
dataVolumeName = R.realName;
}
R.dataVolumeName = dataVolumeName;
}
return R.dataVolumeName;
},
parseDisk(R, index) {
const out = { name: R.name };
@@ -1052,9 +1035,7 @@ export default {
},
parseVolumeClaimTemplate(R, dataVolumeName) {
const sizeString = String(R.size);
if (!(sizeString.includes('Gi') || sizeString.includes('Ti')) && R.size) {
if (!String(R.size).includes('Gi') && R.size) {
R.size = `${ R.size }${ GIBIBYTE }`;
}
@@ -1545,49 +1526,6 @@ export default {
this.refreshYamlEditor();
},
mergeInterfaceList(specInterfaceList, interfaceList) {
if (!specInterfaceList || specInterfaceList.length === 0) {
return interfaceList;
}
const specInterfaceMap = new Map(specInterfaceList.map((iface) => [iface.name, iface]));
return interfaceList.map((iface) => {
const specInterface = specInterfaceMap.get(iface.name);
if (specInterface) {
const merged = { ...specInterface, ...iface };
// currently we only have bridge and masquerade network type, they are mutually exclusive
if (iface['bridge']) {
delete merged['masquerade'];
} else {
delete merged['bridge'];
}
return merged;
}
return iface;
});
},
mergeDeviceList(specDeviceList, deviceList) {
if (!specDeviceList || specDeviceList.length === 0) {
return deviceList;
}
const specDeviceMap = new Map(specDeviceList.map((device) => [device.name, device]));
return deviceList.map((device) => {
const specDevice = specDeviceMap.get(device.name);
if (specDevice) {
return { ...specDevice, ...device };
}
return device;
});
},
refreshYamlEditor() {
this.$nextTick(() => {
this.$refs.yamlEditor?.updateValue();
@@ -1647,7 +1585,7 @@ export default {
secretRef: {
handler(secret) {
if (secret && this.resourceType !== HCI.BACKUP && this.resourceType !== HCI.VM) {
if (secret && this.resourceType !== HCI.BACKUP) {
this.secretName = secret?.metadata.name;
}
},
@@ -1,116 +0,0 @@
import { escapeHtml } from '@shell/utils/string';
import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
import HarvesterResource from './harvester';
/**
* Class representing vGPU MIGConfiguration resource.
* @extends HarvesterResource
*/
export default class MIGCONFIGURATION extends HarvesterResource {
get _availableActions() {
let out = super._availableActions;
out = out.map((action) => {
if (action.action === 'showConfiguration') {
return { ...action, enabled: !this.spec.enabled };
} else if (action.action === 'goToEditYaml') {
return { ...action, enabled: !this.spec.enabled };
} else if (action.action === 'goToEdit') {
// need to wait for status to be disabled or empty value, then allow user to editConfig
return { ...action, enabled: !this.spec.enabled && ['disabled', ''].includes(this.configStatus) };
} else {
return action;
}
});
out.push(
{
action: 'enableConfig',
enabled: !this.isEnabled,
icon: 'icon icon-fw icon-dot',
label: 'Enable',
},
{
action: 'disableConfig',
enabled: this.isEnabled,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable',
},
);
return out;
}
get canYaml() {
return false;
}
get disableResourceDetailDrawer() {
return true;
}
get canDelete() {
return false;
}
get configStatus() {
return this.status.status;
}
get actualState() {
return this.isEnabled ? 'Enabled' : 'Disabled';
}
get stateDisplay() {
return this.actualState;
}
get stateColor() {
const state = this.actualState;
return colorForState(state);
}
get isEnabled() {
return this.spec.enabled;
}
get configuredProfiles() {
const configuredProfiles = this.spec?.profileSpec?.filter((p) => p.requested > 0) || [];
if (configuredProfiles.length === 0) {
return '';
}
return configuredProfiles
.map((profile) => `${ profile.name } * ${ profile.requested }`)
.join(', ');
}
async enableConfig() {
try {
this.spec.enabled = true;
await this.save();
} catch (err) {
this.$dispatch('growl/fromError', {
title: this.t('generic.notification.title.error', { name: escapeHtml(this.name) }),
err,
}, { root: true });
}
}
async disableConfig() {
const { enabled: currentEnabled } = this.spec;
try {
this.spec.enabled = false;
await this.save();
} catch (err) {
this.spec.enabled = currentEnabled;
this.$dispatch('growl/fromError', {
title: this.t('generic.notification.title.error', { name: escapeHtml(this.name) }),
err,
}, { root: true });
}
}
}
@@ -35,10 +35,15 @@ export default class HciPv extends HarvesterResource {
get availableActions() {
let out = super._availableActions;
const clone = out.find((action) => action.action === 'goToClone');
// Longhorn V2 provisioner do not support volume clone feature yet
if (this.isLonghornV2) {
out = out.filter((action) => action.action !== 'goToClone');
} else {
const clone = out.find((action) => action.action === 'goToClone');
if (clone) {
clone.action = 'goToCloneVolume';
if (clone) {
clone.action = 'goToCloneVolume';
}
}
const exportImageAction = {
@@ -60,6 +65,10 @@ export default class HciPv extends HarvesterResource {
takeSnapshotAction,
...out
];
// TODO: remove this block if Longhorn V2 engine supports restore volume snapshot
if (this.isLonghornV2) {
out = out.filter((action) => action.action !== takeSnapshotAction.action);
}
} else { // v1.4 / v1.3
if (!this.isLonghorn || !this.isLonghornV2) {
out = [
@@ -2,7 +2,7 @@ import SteveModel from '@shell/plugins/steve/steve-class';
import { HCI } from '@shell/config/labels-annotations';
import { NETWORK_TYPE } from '../config/types';
const { UNTAGGED, OVERLAY, L2TRUNK_VLAN } = NETWORK_TYPE;
const { UNTAGGED } = NETWORK_TYPE;
export default class NetworkAttachmentDef extends SteveModel {
applyDefaults() {
@@ -29,16 +29,6 @@ export default class NetworkAttachmentDef extends SteveModel {
}
}
get isSystem() {
const systemNamespaces = this.$rootGetters['systemNamespaces'];
if (systemNamespaces.includes(this.metadata?.namespace)) {
return true;
}
return false;
}
get isIpamStatic() {
return this.parseConfig.ipam?.type === 'static';
}
@@ -55,7 +45,7 @@ export default class NetworkAttachmentDef extends SteveModel {
}
get vlanId() {
return this.vlanType === UNTAGGED || this.vlanType === OVERLAY || this.vlanType === L2TRUNK_VLAN ? 'N/A' : this.parseConfig.vlan;
return this.vlanType === UNTAGGED ? 'N/A' : this.parseConfig.vlan;
}
get customValidationRules() {
@@ -78,7 +68,7 @@ export default class NetworkAttachmentDef extends SteveModel {
const route = annotations[HCI.NETWORK_ROUTE];
let config = {};
if (this.vlanType === UNTAGGED || this.vlanType === OVERLAY || this.vlanType === L2TRUNK_VLAN) {
if (this.vlanType === UNTAGGED) {
return 'N/A';
}
@@ -14,7 +14,6 @@ import { BACKUP_TYPE } from '../config/types';
import { HCI } from '../types';
import HarvesterResource from './harvester';
import { getVmCPUMemoryValues } from '../utils/cpuMemory';
import { isBackupTargetSettingUnavailable } from '../utils/setting';
export const OFF = 'Off';
@@ -87,11 +86,17 @@ const IgnoreMessages = ['pod has unbound immediate PersistentVolumeClaims'];
export default class VirtVm extends HarvesterResource {
get availableActions() {
const out = super._availableActions;
const clone = out.find((action) => action.action === 'goToClone');
let out = super._availableActions;
if (clone) {
clone.action = 'goToCloneVM';
// VM attached with Longhorn V2 volume doesn't support clone feature
if (this.longhornV2Volumes.length > 0) {
out = out.filter((action) => action.action !== 'goToClone');
} else {
const clone = out.find((action) => action.action === 'goToClone');
if (clone) {
clone.action = 'goToCloneVM';
}
}
return [
@@ -147,13 +152,13 @@ export default class VirtVm extends HarvesterResource {
},
{
action: 'backupVM',
enabled: !!this.actions?.backup && !this.isBackupTargetUnavailable,
enabled: !!this.actions?.backup,
icon: 'icon icon-backup',
label: this.t('harvester.action.backup')
},
{
action: 'takeVMSnapshot',
enabled: (!!this.actions?.snapshot || !!this.action?.backup),
enabled: (!!this.actions?.snapshot || !!this.action?.backup) && !this.longhornV2Volumes.length,
icon: 'icon icon-snapshot',
label: this.t('harvester.action.vmSnapshot')
},
@@ -200,16 +205,10 @@ export default class VirtVm extends HarvesterResource {
label: this.t('harvester.action.abortMigration')
},
{
action: 'addHotplugVolume',
action: 'addHotplug',
enabled: !!this.actions?.addVolume,
icon: 'icon icon-plus',
label: this.t('harvester.action.addHotplugVolume')
},
{
action: 'addHotplugNic',
enabled: this.hotplugNicFeatureEnabled && !!this.actions?.addNic,
icon: 'icon icon-plus',
label: this.t('harvester.action.addHotplugNic')
label: this.t('harvester.action.addHotplug')
},
{
action: 'createTemplate',
@@ -395,20 +394,8 @@ export default class VirtVm extends HarvesterResource {
this.$dispatch('promptModal', {
resources,
name: diskName,
type: 'volume',
component: 'HarvesterHotUnplug',
});
}
unplugNIC(networkName) {
const resources = this;
this.$dispatch('promptModal', {
resources,
name: networkName,
type: 'network',
component: 'HarvesterHotUnplug',
diskName,
component: 'HarvesterUnplugVolume'
});
}
@@ -516,17 +503,10 @@ export default class VirtVm extends HarvesterResource {
});
}
addHotplugVolume(resources = this) {
addHotplug(resources = this) {
this.$dispatch('promptModal', {
resources,
component: 'HarvesterAddHotplugVolumeModal'
});
}
addHotplugNic(resources = this) {
this.$dispatch('promptModal', {
resources,
component: 'HarvesterAddHotplugNic'
component: 'HarvesterAddHotplugModal'
});
}
@@ -1255,19 +1235,8 @@ export default class VirtVm extends HarvesterResource {
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
}
get vmMachineTypeAutoFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('vmMachineTypeAuto');
}
get hotplugNicFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('hotplugNic');
}
get isBackupTargetUnavailable() {
const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || [];
const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target');
return isBackupTargetSettingUnavailable(backupTargetSetting);
get vmMachineTypesFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('vmMachineTypes');
}
setInstanceLabels(val) {
@@ -16,10 +16,7 @@ export default class HciVlanConfig extends HarvesterResource {
uplink: {
nics: [],
linkAttributes: {},
bondOptions: {
mode: 'active-backup',
miimon: -1
},
bondOptions: { mode: 'active-backup' },
},
};
+2 -2
View File
@@ -1,13 +1,13 @@
{
"name": "harvester",
"description": "Rancher UI Extension for Harvester",
"version": "1.7.0-rc4",
"version": "1.6.1",
"private": false,
"rancher": {
"annotations": {
"catalog.cattle.io/display-name": "Harvester",
"catalog.cattle.io/kube-version": ">= 1.16.0-0",
"catalog.cattle.io/rancher-version": ">= 2.13.0-0",
"catalog.cattle.io/rancher-version": ">= 2.12.0-0",
"catalog.cattle.io/ui-extensions-version": ">= 3.0.0 < 4.0.0"
}
},
@@ -12,7 +12,6 @@ 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';
import { STORAGE_CLASS } from '@shell/config/types';
const IMAGE_METHOD = {
NEW: 'new',
@@ -33,7 +32,6 @@ export default {
async fetch() {
await this.$store.dispatch('harvester/findAll', { type: HCI.IMAGE });
await this.$store.dispatch('harvester/findAll', { type: STORAGE_CLASS });
const value = await this.$store.dispatch('harvester/create', {
type: HCI.UPGRADE,
@@ -65,7 +63,6 @@ export default {
sourceType: UPLOAD,
uploadController: null,
uploadResult: null,
storageClassValue: null,
imageValue: null,
enableLogging: true,
IMAGE_METHOD,
@@ -82,6 +79,7 @@ export default {
skipSingleReplicaDetachedVolFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
},
allOSImages() {
return this.$store.getters['harvester/all'](HCI.IMAGE).filter((I) => I.isOSImage) || [];
},
@@ -118,7 +116,7 @@ export default {
},
fileName() {
return this.preprocessImageName(this.file?.name || '');
return this.file?.name || '';
},
canEnableLogging() {
@@ -183,38 +181,6 @@ export default {
});
},
async createImageStorageClass(imageName = '') {
// delete related SC if existed
await this.deleteImageStorageClass(imageName);
const storageClassPayload = {
apiVersion: 'storage.k8s.io/v1',
type: STORAGE_CLASS,
metadata: { name: imageName },
volumeBindingMode: 'Immediate',
reclaimPolicy: 'Delete',
allowVolumeExpansion: true, // must be boolean type
provisioner: 'driver.longhorn.io',
};
this.storageClassValue = await this.$store.dispatch('harvester/create', storageClassPayload);
if (this.storageClassValue && this.storageClassValue.save) {
await this.storageClassValue.save();
}
},
async deleteImageStorageClass(imageName = '') {
const inStore = this.$store.getters['currentProduct'].inStore;
const storageClasses = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
const targetSC = storageClasses.find((sc) => sc.id === imageName);
if (targetSC && targetSC.remove) {
await targetSC.remove();
}
},
async initImageValue() {
this.imageValue = await this.$store.dispatch('harvester/create', {
type: HCI.IMAGE,
@@ -225,7 +191,6 @@ export default {
annotations: {}
},
spec: {
backend: 'cdi',
sourceType: UPLOAD,
displayName: '',
checksum: this.imageValue?.spec?.checksum || '',
@@ -238,9 +203,8 @@ export default {
this.file = {};
this.errors = [];
const imageDisplayName = this.imageValue?.spec?.displayName || '';
if (!imageDisplayName && this.createNewImage) {
if (!this.imageValue.spec.displayName && this.createNewImage) {
this.errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('generic.name') }));
buttonCb(false);
@@ -248,14 +212,14 @@ export default {
}
try {
// Save the image first if creating a new one
if (this.imageSource === IMAGE_METHOD.NEW) {
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
if (this.sourceType === UPLOAD && this.uploadImageId !== '') { // upload new image
if (this.sourceType === UPLOAD && this.uploadImageId !== '') {
this.value.spec.image = this.uploadImageId;
} else if (this.sourceType === DOWNLOAD) { // give URL to download new image
// check if URL is provided
} else if (this.sourceType === DOWNLOAD) {
this.imageValue.spec.sourceType = DOWNLOAD;
if (!this.imageValue.spec.url) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.imageUrl'));
buttonCb(false);
@@ -263,16 +227,10 @@ export default {
return;
}
// create related image storage class first
await this.createImageStorageClass(imageDisplayName);
this.imageValue.spec.sourceType = DOWNLOAD;
this.imageValue.spec.targetStorageClassName = imageDisplayName;
res = await this.imageValue.save();
this.value.spec.image = res.id;
}
} else if (this.imageSource === IMAGE_METHOD.EXIST) { // select existing image
} else if (this.imageSource === IMAGE_METHOD.EXIST) {
if (!this.imageId) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.chooseFile'));
buttonCb(false);
@@ -282,26 +240,25 @@ export default {
this.value.spec.image = this.imageId;
}
// enable logging or skip single replica detection if checked
if (this.canEnableLogging) {
this.value.spec.logEnabled = this.enableLogging;
}
if (this.skipSingleReplicaDetachedVolFeatureEnabled) {
this.value.metadata.annotations = { [HCI_ANNOTATIONS.SKIP_SINGLE_REPLICA_DETACHED_VOL]: JSON.stringify(this.skipSingleReplicaDetachedVol) };
}
await this.value.save();
this.done();
buttonCb(true);
} catch (e) {
this.errors = [e?.message] || exceptionToErrorsArray(e);
buttonCb(false);
// if anything failed, delete the created image storage class
await this.deleteImageStorageClass(imageDisplayName);
}
},
async uploadFile(file) {
const fileName = this.preprocessImageName(file.name);
const fileName = file.name;
if (!fileName) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.unknownImageName'));
@@ -325,10 +282,6 @@ export default {
this.imageValue.spec.url = '';
try {
// before uploading image, we need to create related image storage class first
await this.createImageStorageClass(fileName);
this.imageValue.spec.targetStorageClassName = fileName;
const res = await this.imageValue.save();
this.uploadImageId = res.id;
@@ -345,35 +298,20 @@ export default {
} else {
this.errors = exceptionToErrorsArray(e);
}
// if upload failed, delete the created image storage class
await this.deleteImageStorageClass(fileName);
this.file = {};
this.uploadImageId = '';
}
},
// replace _ to - to meet storage class name requirement
preprocessImageName(name) {
if (!name) {
return '';
}
return name.toLowerCase().replace(/[_]/g, '-');
},
handleImageDelete(imageId) {
const image = this.allOSImages.find((I) => I.id === imageId);
const imageDisplayName = image?.spec?.displayName || '';
if (image && imageDisplayName) {
if (image) {
this.$store.dispatch('harvester/promptModal', {
resources: [image],
component: 'ConfirmRelatedToRemoveDialog',
needConfirmation: false,
warningMessage: this.$store.getters['i18n/t']('harvester.modal.osImage.message', { name: imageDisplayName }),
extraActionAfterRemove: async() => {
await this.deleteImageStorageClass(imageDisplayName);
}
resources: [image],
component: 'ConfirmRelatedToRemoveDialog',
needConfirmation: false,
warningMessage: this.$store.getters['i18n/t']('harvester.modal.osImage.message', { name: image.displayName })
});
this.deleteImageId = '';
}
@@ -396,22 +334,6 @@ export default {
},
watch: {
async sourceType(neu) {
if (neu === DOWNLOAD && this.imageValue && this.uploadController) {
if (this.uploadController) {
this.uploadController.abort();
}
try {
await this.imageValue.remove();
await this.initImageValue();
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error occurred while removing imageValue:', error);
}
}
},
imageSource(neu) {
if (neu !== IMAGE_METHOD.DELETE) {
this.deleteImageId = '';
@@ -483,13 +405,13 @@ export default {
v-if="showUploadSuccessBanner"
color="success"
class="mt-0 mb-30"
:label="t('harvester.setting.upgrade.uploadSuccess', { name: fileName })"
: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: fileName })"
:label="t('harvester.image.warning.osUpgrade.uploading', { name: file.name })"
/>
<div
+1 -3
View File
@@ -37,7 +37,6 @@ export const HCI = {
STORAGE: 'harvesterhci.io.storage',
RESOURCE_QUOTA: 'harvesterhci.io.resourcequota',
KSTUNED: 'node.harvesterhci.io.ksmtuned',
HUGEPAGES: 'node.harvesterhci.io.hugepage',
PCI_DEVICE: 'devices.harvesterhci.io.pcidevice',
PCI_CLAIM: 'devices.harvesterhci.io.pcideviceclaim',
SR_IOV: 'devices.harvesterhci.io.sriovnetworkdevice',
@@ -45,7 +44,6 @@ export const HCI = {
SR_IOVGPU_DEVICE: 'devices.harvesterhci.io.sriovgpudevice',
USB_DEVICE: 'devices.harvesterhci.io.usbdevice',
USB_CLAIM: 'devices.harvesterhci.io.usbdeviceclaim',
MIG_CONFIGURATION: 'devices.harvesterhci.io.migconfiguration',
VLAN_CONFIG: 'network.harvesterhci.io.vlanconfig',
VLAN_STATUS: 'network.harvesterhci.io.vlanstatus',
ADD_ONS: 'harvesterhci.io.addon',
@@ -55,7 +53,7 @@ export const HCI = {
LB: 'loadbalancer.harvesterhci.io.loadbalancer',
IP_POOL: 'loadbalancer.harvesterhci.io.ippool',
HARVESTER_CONFIG: 'rke-machine-config.cattle.io.harvesterconfig',
LVM_VOLUME_GROUP: 'harvesterhci.io.lvmvolumegroup',
LVM_VOLUME_GROUP: 'harvesterhci.io.lvmvolumegroup'
};
export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot';
-25
View File
@@ -1,25 +0,0 @@
export function isBackupTargetSettingEmpty(setting = {}) {
let isEmpty = true;
if (setting?.value) {
try {
const valueJson = JSON.parse(setting?.value);
isEmpty = !valueJson.type;
} catch (e) {
// eslint-disable-next-line no-console
console.error('Failed to parse backup target value:', e);
}
}
return isEmpty;
}
export function isBackupTargetSettingUnavailable(setting = {}) {
const errorMessage = setting.errorMessage;
const isEmptyValue = isBackupTargetSettingEmpty(setting);
const canUpdate = setting.canUpdate;
return (errorMessage || isEmptyValue) && canUpdate;
}
+6 -54
View File
@@ -2,78 +2,30 @@ import { HCI } from '@pkg/harvester/config/labels-annotations';
export const VM_IMAGE_FILE_FORMAT = ['qcow', 'qcow2', 'raw', 'img', 'iso'];
/**
* Extracts the filename from a URL, handling query parameters and fragments
* @param {string} url - The URL to parse
* @returns {string} - The filename without query params or fragments
*/
function getFilenameFromUrl(url) {
try {
// Try to parse as a full URL
const urlObj = new URL(url);
// Get pathname and extract the last segment
const pathname = urlObj.pathname;
return pathname.split('/').filter(Boolean).pop() || '';
} catch (e) {
// If URL parsing fails, treat as a relative path
// Remove query params and fragments manually
const cleanUrl = url.split('?')[0].split('#')[0];
return cleanUrl.split('/').pop() || '';
}
}
/**
* Validates image URL format
* @param {string} url - The image URL to validate
* @param {object} getters - Vuex getters
* @param {array} errors - Array to collect validation errors
* @param {any} validatorArgs - Additional validator arguments
* @param {string} type - Type of validation ('file' or other)
* @returns {array} - Array of validation errors
*/
export function imageUrl(url, getters, errors, validatorArgs, type) {
const tipString =
type === 'file' ? 'harvester.validation.image.ruleFileTip' : 'harvester.validation.image.ruleTip';
const t = getters['i18n/t'];
if (!url || url === '') {
return errors;
}
// Extract filename, handling query parameters and fragments
const filename = getFilenameFromUrl(url);
if (!filename) {
errors.push(t(tipString));
return errors;
}
// Get file extension
const fileSuffix = filename.split('.').pop().toLowerCase();
const suffixName = url.split('/').pop();
const fileSuffix = suffixName.split('.').pop().toLowerCase();
if (!VM_IMAGE_FILE_FORMAT.includes(fileSuffix)) {
const tipString = type === 'file' ? 'harvester.validation.image.ruleFileTip' : 'harvester.validation.image.ruleTip';
errors.push(t(tipString));
}
return errors;
}
export function fileRequired(
annotations = {},
getters,
errors,
validatorArgs,
type
) {
export function fileRequired(annotations = {}, getters, errors, validatorArgs, type) {
const t = getters['i18n/t'];
if (!annotations[HCI.IMAGE_NAME]) {
errors.push(
t('validation.required', { key: t('harvester.image.fileName') })
);
errors.push(t('validation.required', { key: t('harvester.image.fileName') }));
}
return errors;
+543 -907
View File
File diff suppressed because it is too large Load Diff