Compare commits

..
313 Commits
Author SHA1 Message Date
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
Andy Lee 8cd482870b chore: downgrade shell to 3.0.5-rc3 (#348)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-26 11:39:16 +08:00
Andy Lee 57cbd799dd fix adding existing volume in edit VM page (#351) 2025-06-26 10:07:15 +08:00
Yiya Chen 3694b316ab fix: dynamic get VM's machine type API endpoint (#349)
* fix: update API endpoint
* fix: remove redudant slash
---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-06-26 09:19:09 +08:00
Nick Chung 717258defd feat: introduce commit-lint (#350)
Signed-off-by: Nick Chung <nick.chung@suse.com>
2025-06-26 08:39:44 +08:00
Andy Lee 7aae6264f7 Add auto assign reviewer workflow (#346)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-25 11:13:18 +08:00
Andy Lee c88a083e04 Upgrade rancher/shell to 3.0.5-rc5 (#344)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-23 14:56:55 +08:00
Yiya Chen 52c4556e64 fix: failed to clone an image file created by file upload (#339)
* fix: filter clone action
* fix: hide clone action for other types
* feat: show source type in details
---------
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-06-18 17:17:43 +08:00
Andy Lee 90cb147938 Align settings page actionMenu style with other pages (#342)
* Align settings page actionMeun with other pages

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

* update based on PR feedback

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-18 15:03:57 +08:00
Andy Lee d9c97de0fd Add searchbox in settings page (#338)
* Add searchbox in setting page

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

* update based on comment

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

* PR feedback

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-17 14:50:52 +08:00
Andy Lee fb59b396d1 remove disabled for upgrade action (#340)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-17 13:51:34 +08:00
Andy Lee bdcea54eeb Fix uploading image stuck at edit page (#336)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-11 16:49:26 +08:00
Andy Lee 912ca7883f Add description column in namespace and image pages (#335)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-11 10:16:34 +08:00
Andy Lee a89678cc81 upgrade shell to 3.0.5-rc3 (#334)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-11 10:16:24 +08:00
Andy Lee d4d3774c3b filter golden image volume count in dashboard (#332)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-10 12:30:21 +08:00
Andy Lee 32ebdc3acd fix http-proxy setting not saved (#329)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-06 17:01:31 +08:00
Andy Lee 703abd7ab9 add auto backport v1.0 mergify rule (#328)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-06 16:59:46 +08:00
Andy Lee e8f7f0e06b fix multiple VM modals (#325) 2025-06-06 15:51:04 +08:00
Yiya Chen feddcd247a feat: add confirmation modal for vm soft reboot and restart (#327)
* feat: add confirmation for vm softreboot and restart

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* refactor: update wordings

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* feat: handle bulk action

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-06-06 12:27:15 +08:00
Yiya Chen 29b1ab2fb9 feat: support manually configuring VM IP (#315)
* feat: add annotations

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* feat: map annotation

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* feat: add annotaion to template

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-06-06 10:03:37 +08:00
Yiya Chen 01528f7429 fix: inherit labels (#322)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-06-04 17:42:57 +08:00
Andy Lee 9e015ec3b1 fix API doesn't bring checksum (#323) 2025-06-03 20:58:03 +08:00
Yiya Chen 7ecc9c320d fix: VM's machine type options is not support (#307)
* feat: get options from API

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* feat: add feature flag

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* refactor: remove default value

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* refactor: fallback to none

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-06-02 12:38:04 +08:00
Andy Lee d6da4898b4 Fix error banner can't be dismissed (#312)
* fix error banner can't be dismissed

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

* update all CruResource

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-29 15:43:14 +08:00
Andy Lee d023104371 simply PR template (#317)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-28 00:09:45 +08:00
Andy Lee cb452b9627 fix upgrade button status if have errors (#316) 2025-05-27 15:24:42 +08:00
Andy Lee dbbad01b0f Add OS upgrade features (#311)
* Add failed and success banner after image uploaded

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

* add delete image feature

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

* add skip checking single-replica detached volumes checkbox

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

* change delete image flow

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

* Reuse ConfirmRelatedToRemoveDialog

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-27 14:29:48 +08:00
Yiya Chen b689e3aacf feat: disable label (#310)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-23 16:23:09 +08:00
Yiya Chen 796d98e61a fix: Harvester upgrade ISO related internal image can be selected to create VM when an upgrade is ongoing (#309)
* fix: filter out upgrade image

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* refactor: add label constant

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-23 15:49:30 +08:00
Andy Lee fe32b4372b give default valeu for uploadImage opt (#308)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-22 16:22:13 +08:00
Yiya Chen 3dcec52aff fix: hide error message (#305)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-22 11:07:20 +08:00
Andy Lee 6d8f6579c7 Fix OS upgrade page (#306)
* fix OS upgrade page

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

* move uploadFile action

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

* change os upgrage ui to standard feature

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

* Add uploadImage cancel

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
Signed-off-by: a110605 <andy.lee@suse.com>
2025-05-21 16:51:16 +08:00
Yiya ChenandAndy Lee 9e240792e8 doc: add QA testing guideline (#303)
* doc: add testing guideline

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* doc: update wording

Co-authored-by: Andy Lee <andy.lee@suse.com>
Signed-off-by: Yiya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Signed-off-by: Yiya Chen <yiya.chen@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-05-19 12:24:52 +08:00
Andy Lee aa9dd32cce Change isKVMDisable condition (#294)
* change condition to check isKVMDisable

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

* change condition eq 0

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

* add allNotExist back

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-19 10:17:07 +08:00
Yiya Chen 4f3e532327 chore: build main-head on PR merge (#302)
* chore: add main branch

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* ci: add dev branch

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-16 15:42:38 +08:00
Yiya Chen cbfcf4dbae fix: range condition (#299)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-15 16:17:04 +08:00
Andy Lee 8877dcf639 upgrade shell to 3.0.5-rc.2 (#297) 2025-05-15 11:21:06 +08:00
renovate[bot] 8d196c4244 chore(deps): update dependency follow-redirects to v1.15.6 [security] (#285)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-15 10:48:02 +08:00
Yiya Chen 220e40feaa chore: add labels (#295)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-14 17:24:36 +08:00
Yiya ChenandJillian Maroket 258476876a refactor: the promptRemove.confirmRelatedResource message needs to be refactored (#293)
* feat: change dialog wordings

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* refactor: update protip wording

Co-authored-by: Jillian Maroket <67180770+jillian-maroket@users.noreply.github.com>
Signed-off-by: Yiya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Signed-off-by: Yiya Chen <yiya.chen@suse.com>
Co-authored-by: Jillian Maroket <67180770+jillian-maroket@users.noreply.github.com>
2025-05-14 15:27:06 +08:00
Andy Lee 63c4810b99 Reset network / user script when choosing ostype from windows to linux (#292)
* reset network / user script when choosing windows

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

* change OS type from Windows init user and network data

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-14 14:15:39 +08:00
Andy Lee 0cd5651c38 upgrade shell to 3.0.5-rc1 (#282)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-13 15:33:50 +08:00
renovate[bot] 6f66c80c63 chore(deps): update dependency ejs to v3.1.10 [security] (#284)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-13 15:25:11 +08:00
Yiya Chen 0e5a78d8a6 chore: add renovate (#283)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-12 05:54:35 -05:00
Andy Lee d16802365e Add live migration progress awareness in migration tab (#271)
* Embedded VM migration detail in migration tab

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

* update events condition

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-12 15:04:50 +08:00
Andy Lee 8410f75859 Remove unneeded condition (#279)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-12 10:48:01 +08:00
Andy Lee f8079c5924 Refactor feature flags structure (#276)
* refactor feature flags structure

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

* Replace with FEATURE_FLAGS and valid and sort versions

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-12 10:43:12 +08:00
mergify[bot]andAndy Lee 1e270268d8 fix #cloud-config disappear when add/remove ssh key (#273) (#277)
(cherry picked from commit 9dd8ae479a)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-05-08 17:34:15 +08:00
Yiya Chen 43064576d6 refactor: disable required (#274)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-08 15:17:01 +08:00
Yiya Chen 9dd36bbd87 fix: skip escapehtml (#272)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-08 15:12:54 +08:00
Andy Lee 235373045c Add some feature flags (#270)
* Add 1.4.2/1.5.1/1.6.0 feature flags

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

* bump version to v1.6.0-dev

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-08 13:18:45 +08:00
Yiya Chen 5841508b26 fix: hide btns before is ready (#267) 2025-05-05 20:49:08 +08:00
Yiya Chen b941902088 fix: support-bundle-expiration can be invalid value 000003333 (#248)
* fix: normalize number input

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

* fix: leverage to other inputs

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-04-28 11:45:50 +08:00
Andy Lee 485db3066f add require backport 1.5 label in mergify.yml (#265)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-28 11:36:26 +08:00
Andy Lee af43ebbf6f bump to 1.5.0 (#263)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-23 17:10:48 +08:00
Andy Lee 74300f24b3 update RadioGroup update value action (#255)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-17 15:26:07 +08:00
Andy Lee 2993ddb82c fix unable to detach PCI & USB device in VM edit page (#247)
* fix unable to detach PCI & USB device in VM edit page

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

* refactor based on comment

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

* using set from shell/utils/object

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-16 16:09:33 +08:00
Andy Lee a5d4604dce bump to 1.5.0-rc5 (#245) 2025-04-16 00:19:35 +08:00
Andy Lee daa6d6942b pass showYaml prop to prevent double generate secret (#243)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-15 16:05:13 +08:00
Andy Lee eb92642b3b Add logic to block import harvester version lower than 1.3.0 (#241)
* add logic to block import harvester version lower than 1.3.0

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

* refactor console.error message

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-10 16:21:17 +08:00
Andy Lee 78234b9e1e filter golden image volume in hot plug volume modal (#239)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-10 13:17:30 +08:00
Andy Lee 3dc6dda1ca bump to 1.5.0-rc4 (#235)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-09 14:04:12 +08:00
Andy Lee fca05d8489 hide usb device tab in v1.3.x (#236)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-09 14:01:36 +08:00
Andy Lee 238c660796 Fix undefined error in VM edit as yaml page (#233)
* fix undefined error whenclick save in  edit as yaml

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

* fix VM isEqual logic and fix undefined error

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-07 15:30:59 +08:00
Andy Lee 19bea97106 fix secret name is different when edit as yaml (#230)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-07 10:47:50 +08:00
Jack Yu de4edbbf3b chore: use default imag repo name (#212)
Signed-off-by: Jack Yu <jack.yu@suse.com>
2025-04-06 21:08:23 -05:00
Andy Lee 9256391627 Bump ui-ext to 1.5.0-rc3 (#228)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-02 16:00:39 +08:00
Andy Lee 25439aee65 Fix error banner position in all dialogs (#226)
* fix error banner position in all dialogs

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

* update HarvesterImageDownloader.vue

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-31 16:27:13 +08:00
Andy Lee f2fe1f5bd6 change longhorn v2 access mode to RWX (#223) 2025-03-28 12:38:30 +08:00
Andy Lee f8d5aa1a21 update to take VM snapshot logic (#222) 2025-03-28 12:37:54 +08:00
Andy Lee 38cf667830 use harvester translation key forhost page CPU and memory (#220)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-27 13:24:55 +08:00
Andy Lee 18667836c2 upgrade ui-ext version to 1.5.0-rc2 (#208)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-25 11:42:16 +08:00
Andy Lee 2940f25fe0 fix volume won't be dismiss after deleting (#216)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-24 15:31:14 +08:00
Andy Lee a24d01717a fix image.repo to image.repository (#214)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-21 17:40:39 +08:00
Andy Lee 22a032e56c fix the doneOverride path for harvesterhci.io.virtualmachinetemplate (#211)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-20 17:06:06 +08:00
Andy Lee 9343813ace Add volum mode options in create VM volume tab (#209)
* add volume mode option for non-longhorn volume

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

* fix default storage class in create VM page

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-19 17:42:02 +08:00
Andy Lee 2dff7b0a93 upgrade rancher/shell to 3.0.2-rc6 (#206)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-14 16:47:04 +08:00
Andy Lee 54c5d77198 fine tune clone VM dialog error message (#203)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-14 15:09:16 +08:00
Jack Yu 1880043a80 chore: add building catalog GA (#181)
* chore: add building catalog GA

Signed-off-by: Jack Yu <jack.yu@suse.com>

* feat: should include prerelease

Signed-off-by: Jack Yu <jack.yu@suse.com>

---------

Signed-off-by: Jack Yu <jack.yu@suse.com>
2025-03-14 15:03:00 +08:00
Andy Lee d8bee7f4f5 Bring status object in harvesterhci.io.upgrade dismiss PUT API (#201)
* bring status object in PUT dismiss API

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

* add cleanForSave in harvester model

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-13 14:10:59 +08:00
Andy Lee 90c923b480 Add volumeMode dropdown for third-party storage (#197)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-12 14:25:37 +08:00
Andy Lee 374b904191 replace tag format v1.5.0 to harvester-1.5.0 (#190)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-12 12:00:09 +08:00
Andy Lee f2725e2773 Merge pull request #196 from a110605/fix_progresslist_typo
fix percent props typo
2025-03-11 16:53:55 +08:00
Andy Lee 487f9abc10 fix percent typo
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-11 14:52:50 +08:00
Andy Lee 1862cfcc08 Merge pull request #191 from a110605/add_box
Fix missing remove button in Infobox
2025-03-11 11:58:35 +08:00
Andy Lee 480f95a5cc Merge pull request #192 from a110605/remove_require_dot
remove require dot for non-longhorn v1
2025-03-11 11:34:30 +08:00
Andy Lee b939df9b7d remove require dot for non-longhorn v1
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-11 11:14:27 +08:00
Andy Lee 4e831cbc5f Fix infobox style
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-07 14:54:58 +08:00
Andy Lee 0283cfb2bb Merge pull request #183 from a110605/bump_rc1
Bump harvester-ui-extension to v1.5.0-rc1
2025-03-06 15:55:29 +08:00
Yiya Chen 4e2562190c Merge pull request #185 from houhoucoop/issue-7737
fix: allow set empty for backupVolumeSnapshotClassName in Setting csi-driver-config
2025-03-06 15:48:46 +08:00
Yi-Ya Chen 64b59a0c27 refactor: default should selectable
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-03-06 15:34:45 +08:00
Andy Lee 3e7b2338ff Merge pull request #186 from a110605/filter_export_lhv1
Filter exported SC for non-longhorn V1 volume
2025-03-06 15:34:33 +08:00
Andy Lee 104e98e390 Filter export SC for non-longhorn V1 volume
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-06 15:21:50 +08:00
Yi-Ya Chen 72415622d9 feat: gray out class name selector
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-03-06 15:00:58 +08:00
Andy Lee a861450874 Merge pull request #182 from a110605/add_image_downloader
Add HarvesterImageDownloader for cdi image
2025-03-06 14:49:24 +08:00
Andy Lee cbd5e45200 Remove converting sentence
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-06 14:39:50 +08:00
Andy Lee 8ad7a57ab4 bump to v1.5.0-rc1
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-06 11:53:49 +08:00
Andy Lee 966d4d6709 Add HarvesterImageDownloader
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-06 11:14:43 +08:00
Andy Lee 208f1f29f5 Merge pull request #175 from a110605/issue_5974_2
Add label tab to vm template
2025-03-04 17:35:24 +08:00
Andy Lee d923239b9d Merge pull request #178 from a110605/fix_error
Fix  undefined reading harvesterhci.io/storageClassName
2025-03-04 11:01:28 +08:00
Andy Lee 54f85963ea fix undefined reading harvesterhci.io/storageClassName
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-04 10:59:35 +08:00
Andy Lee 36257299e1 Merge pull request #173 from a110605/fix_stopping
Introduce VM Pending status
2025-03-04 10:48:35 +08:00
Andy Lee 91d078068f Merge pull request #174 from a110605/align_gi
fix: align all memory / storage / quota units to Gi/Mi
2025-03-04 10:46:59 +08:00
Andy Lee 8fbbfc90f1 update harvesterhci.io.host
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-04 10:43:12 +08:00
Andy Lee efdfdbf0b8 Add label tab to vm template
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-03 17:14:30 +08:00
Andy Lee 8db4de1c4a Extract variables to utils/unit
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-03 16:57:25 +08:00
Andy Lee 05ff8e4f19 introduce VM pending status
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-03 14:46:35 +08:00
Andy Lee 8f76d5ad30 fix: align all memory / storage / quota units to Gi/Mi
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-02-28 22:57:54 +08:00
Andy Lee ff4a865c55 Merge pull request #171 from a110605/feature_flag_refresh
Add refreshIntervalInSecond in 1.4.2 feature flag
2025-02-27 17:23:56 +08:00
andy.lee 30dc56cad5 add refreshIntervalInSecond in 1.4.2 feature flag
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-27 17:22:17 +08:00
Andy Lee c10573d24a Merge pull request #169 from a110605/fix_bug_third_party
fix golden image error when creating LVM volume
2025-02-27 13:45:55 +08:00
andy.lee ca7eccd688 fix golden image error when creating LVM volume
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-26 17:48:14 +08:00
Andy Lee fc606dc4d1 Merge pull request #167 from a110605/hide_external_link
Hide non-longhorn volume external link
2025-02-26 17:00:01 +08:00
andy.lee 046947de32 hide non longhorn volume external link
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-26 16:54:09 +08:00
Andy Lee 6e5532e497 Merge pull request #164 from a110605/issue_7693
Add refreshIntervalInSeconds in backup targe edit page
2025-02-26 16:17:30 +08:00
andy.lee 7e00a08e35 Add refreshIntervalInSeconds in backup targe edit page
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-26 15:53:13 +08:00
Andy Lee 48ec40fe3d Merge pull request #162 from a110605/fix_upgrade_modal
Fix upgrade cluster model styles
2025-02-26 09:31:32 +08:00
andy.lee 50901a2eaf change overallMessage color to red
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-25 15:45:45 +08:00
andy.lee ff439e66a9 fix upgrade model style
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-25 15:15:26 +08:00
Andy Lee b0b5e5f749 Merge pull request #159 from a110605/issue_5974
Add Labels Tab in VM edit page
2025-02-24 17:41:06 +08:00
Andy Lee be2db9d37c Merge pull request #152 from a110605/issue_7354
Fix usb and pci devices unable to passthrough
2025-02-24 16:40:08 +08:00
andy.lee 9cd3cd6492 Add labels tab in VM edit pae
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-24 14:56:48 +08:00
andy.lee e07042b9c9 update catalog.cattle.io/rancher-version to 2.11.0
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-21 16:56:29 +08:00
Andy Lee 799252a789 Merge pull request #157 from a110605/update_min_rancher
Limit harvester-ui-extension to install on Rancher 2.11.0+
2025-02-21 16:38:30 +08:00
andy.lee 36c68eefb1 Update catalog.cattle.io/rancher-version to 2.11.0
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-21 16:03:41 +08:00
Andy Lee dea3c098df Merge pull request #154 from a110605/bump_ver_v150
Bump harvester-ui-extension version to v1.5.0-dev
2025-02-21 15:00:51 +08:00
andy.lee 0a782fa4d5 bump to v1.5.0-dev
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-21 14:39:13 +08:00
Andy Lee 5905448b90 Merge pull request #148 from a110605/update_install_docs
Update installation flow in README.md
2025-02-21 14:13:22 +08:00
Andy Lee 4553bdc666 Merge pull request #151 from houhoucoop/issue-7593
feat: set up a way to test UI changes in Harvester UI when accessed via Rancher
2025-02-21 13:33:28 +08:00
Yi-Ya Chen 7f98dfe9c8 ci: rename file
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-21 12:33:33 +08:00
Yi-Ya Chen 6a3153a7ec ci: revert change
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-21 10:49:26 +08:00
andy.lee 188f058df8 add pcideviceclaim and usbdeviceclaim models
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-20 17:23:31 +08:00
Yi-Ya Chen 629cb0c601 ci: rename the workflow 2025-02-20 16:30:13 +08:00
Yi-Ya Chen 0c3fe22df7 ci: add ci pipeline
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-20 16:22:03 +08:00
Andy Lee f4edfb8f43 Merge pull request #149 from a110605/issue_7572_2
More third party storage UI change
2025-02-20 14:45:05 +08:00
andy.lee 66f53c8a00 More third party storage UI change
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-20 11:35:59 +08:00
andy.lee f855a0add5 Update installation flow in README.md
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-19 17:39:45 +08:00
Andy Lee 7c2317fae9 Merge pull request #145 from a110605/issue_7572
External/third-party storage UI change
2025-02-19 17:04:50 +08:00
Andy Lee 50fcec0c11 Merge pull request #143 from a110605/fix_issue_7632
Fix sshkey is not added to cloud init user data immediately
2025-02-19 16:22:17 +08:00
andy.lee 4a303e514c allow sorting storage class column in volume list page
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-19 14:07:32 +08:00
andy.lee b828c2f66d add thirdPartyStorage feature flag
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-19 13:33:30 +08:00
andy.lee 40794d89a0 add third party storage UI change
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-19 12:59:28 +08:00
Yiya Chen a6a99520a6 Merge pull request #142 from houhoucoop/fix/issue-7578
fix: no progress circle on the top after triggering upgrade
2025-02-19 09:15:16 +08:00
andy.lee e2daa1ea0c fix sshkey is not added to cloud init user data immediately
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-18 17:29:27 +08:00
Andy Lee 923f36a5c3 Merge pull request #140 from a110605/update_yarn_lock
update yarn.lock
2025-02-17 12:13:46 +08:00
Yi-Ya Chen b499f62c56 fix: register upgrade component in header
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-17 12:10:18 +08:00
andy.lee 9d02623b3a update yarn.lock
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-17 12:08:54 +08:00
Andy Lee 762ab100b9 Merge pull request #138 from houhoucoop/fix/issue-7007
fix: Harvester ui extension not working built from CI
2025-02-17 11:56:21 +08:00
Yi-Ya Chen cbe66f4660 feat: fix $ctx redefine issue
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-14 14:39:27 +08:00
Andy Lee 5b39432481 Merge pull request #133 from a110605/upgrade_shell
Upgrade @rancher/shell to v3.0.2-rc3
2025-02-14 13:29:57 +08:00
Andy Lee 9e32052329 Merge pull request #134 from a110605/move_schema
Move harvester schema.js into harvester folder
2025-02-14 13:29:33 +08:00
Yiya Chen a55353fd1b Merge pull request #132 from houhoucoop/feat/issue-7557
feat: add EFI persistent state checkbox in VM advanced tab
2025-02-13 18:03:33 +08:00
andy.lee 548bdd2835 move harvester schema.js into harvester folder
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-13 11:34:44 +08:00
andy.lee 0153f9d985 upgrade @rancher/shell to v3.0.2-rc3
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-12 16:33:06 +08:00
Yi-Ya Chen 70a56c75b0 lint: fix eslint
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-12 15:59:16 +08:00
Yi-Ya Chen 31a9abd5c2 refactor: remove persistent property if unchecked
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-12 15:38:11 +08:00
Yi-Ya Chen 6d5139a994 fix: state not update when the TPM checkbox is toggled
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-12 15:12:08 +08:00
Yi-Ya Chen b92f22fa75 feat: add efi persistent state
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-12 15:05:07 +08:00
Yiya Chen f9a277d893 Merge pull request #128 from houhoucoop/issue-7328
feat: improve Single-Replica Volume Detection for Upgrade
2025-02-12 09:09:33 +08:00
Yi-Ya Chen 3b054b35c6 feat: add divider
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-11 17:57:36 +08:00
Yi-Ya Chen 2493287334 Merge branch 'main' into issue-7328 2025-02-11 17:51:07 +08:00
Andy Lee 9af8526ef9 Merge pull request #127 from a110605/issue_7544
fix: unable to create new sshkey in VM create page
2025-02-11 17:29:43 +08:00
Andy Lee 07e0dc0d5e Merge pull request #119 from a110605/issue_6501
Add VlanL2Network / UntaggedNetwork types in network-storage setting
2025-02-11 17:16:53 +08:00
Yi-Ya Chen fd3f5ec1e6 refactor: align key spacing
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-11 15:41:11 +08:00
Yi-Ya Chen 72c6741af1 feat: add feature flag
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-11 15:38:41 +08:00
Yi-Ya Chen 375127a78a feat: add checkbox to skip checking
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-11 15:09:13 +08:00
andy.lee fc09e030a0 update feature flag
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-11 14:57:49 +08:00
andy.lee 5f76da4629 extract network_type to config/types
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-11 14:51:50 +08:00
andy.lee 3a3122f3a1 fix number of IP formula
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-11 14:51:50 +08:00
andy.lee 229cbf1f69 add network type in storage-network setting
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-11 14:51:50 +08:00
andy.lee 176946e5e9 fix: unable to create new sshkey in VM create page
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-11 14:48:07 +08:00
Andy Lee f3d2d2f6a8 Merge pull request #125 from a110605/issue_7003_ext
fix: vGPU number not correct
2025-02-11 13:13:02 +08:00
andy.lee a175d1e142 remove unneeded translation keys
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-11 11:34:30 +08:00
andy.lee 26afffc2f1 fix: vGPU number not correct
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-11 11:29:23 +08:00
Andy Lee 5eb19106ea Merge pull request #121 from a110605/issue_6341_ext
The hints on the branding page is using the wrong product name
2025-02-11 11:24:31 +08:00
Andy Lee 7b3857cb38 Merge pull request #122 from a110605/issue_7150
fix: isEncrypted breaks all the volume page
2025-02-11 11:20:10 +08:00
andy.lee 2232e56ba1 fix: isEncrypted breaks all the volume page
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-11 11:17:32 +08:00
Volker Theile d023fa1de4 The hints on the branding page is using the wrong product name
The hints on the `Branding` page at `Advanced > Settings > UI > branding` are using the product name `Rancher` instead of `Harvester`.

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

Signed-off-by: Volker Theile <vtheile@suse.com>
2025-02-11 11:09:24 +08:00
Andy Lee 527d31e4d0 Merge pull request #117 from houhoucoop/feat/issue-7187
feat: add support for persistent TPM in VMs
2025-02-11 10:27:50 +08:00
Yi-Ya Chen 1651e201e8 refactor: update feature flag
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-10 18:38:50 +08:00
Yi-Ya Chen 43b30ec3c3 feat: add feature flag
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-07 18:12:20 +08:00
Yi-Ya Chen 68ba934b5a refactor: rewrite render logic
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-07 17:07:00 +08:00
Andy Lee 67754427c7 Merge pull request #116 from a110605/issue_7516
Fix unable to add/delete new hard disk in edit node page
2025-02-07 11:23:08 +08:00
andy.lee cbb66175f6 fix unable to add/delete new hard disk
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-06 21:41:25 +08:00
Yi-Ya Chen 84bacfaff5 feat: add checkbox to create templates
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-06 18:49:34 +08:00
Yi-Ya Chen e8017e14c8 feat: add persistent state checkbox
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-02-06 18:34:58 +08:00
Andy Lee d11d9d4e37 Merge pull request #115 from a110605/fix_cpu_pinnging
Fix align CPU pinning checkbox position in cloud template
2025-02-06 14:36:18 +08:00
andy.lee 50bdd53186 align CPU pinning checkbox position in cloud template
(cherry picked from commit 59bf735f8b)
2025-02-06 14:24:51 +08:00
Andy Lee 5c96e90d58 Merge pull request #110 from a110605/issue_7510
Prevent display lock icon if this.volumes is empty array
2025-02-06 11:47:35 +08:00
Andy Lee 5c06287648 Merge pull request #109 from a110605/add_v142_feature_flag
Add v1.4.2 feature flag to master branch
2025-02-05 17:44:28 +08:00
andy.lee c75755a527 prevent display icon if this.volumes is empty array
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-05 14:06:12 +08:00
andy.lee e8f282395e add v1.4.2 feature flag
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-05 13:55:30 +08:00
Andy Lee 2bb65801d9 Merge pull request #107 from a110605/issue-7283
upgrade @rancher/shell to v3.0.2-rc2
2025-02-05 12:31:46 +08:00
Francesco Torchia ccd4ff2d7a Revert "Remove lint and bump 1.0.5"
This reverts commit ac6958fe40.
2025-01-27 12:00:35 +01:00
Francesco Torchia ac6958fe40 Remove lint and bump 1.0.5
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2025-01-27 11:59:31 +01:00
andy.lee 426e15084e upgrade @rancher/shell to v3.0.2-rc2
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-24 16:16:14 +08:00
Yiya Chen 8477f0754d Merge pull request #103 from houhoucoop/issue-7295
fix: VM Snapshot and Backup List Views Fail to Load After Creation
2025-01-24 10:07:42 +08:00
Yi-Ya Chen 12cadbf667 fix: get sourceSchedule exception
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-21 14:14:56 +08:00
Yiya Chen b9fab8bb57 Merge pull request #94 from houhoucoop/issue-7382
feat: add confirmation pop-up for VM stop and pause actions
2025-01-21 10:23:54 +08:00
Yiya Chen 69e639ff96 Merge pull request #83 from houhoucoop/issue-7081
fix: cluster member role user should not be able to delete node on Harvester Hosts page
2025-01-21 10:23:17 +08:00
Andy Lee cb4cdcb0c7 Merge pull request #89 from a110605/issue_6592
fix: remove witness node in VM node scheduling tab
2025-01-20 20:00:22 +08:00
Yi-Ya Chen c4e9a89ba5 style: change apply button color
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-20 15:55:17 +08:00
Yi-Ya Chen f2d5172224 feat: handle bulk actions
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-20 12:19:57 +08:00
Andy Lee f0a66774b8 Merge pull request #93 from a110605/issue_6792
fix: encrypt option can only choose encrypted storage class when creating VM image
2025-01-20 11:38:18 +08:00
Yi-Ya Chen e95955fad6 refactor: get value from parent class
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-20 11:13:17 +08:00
Yi-Ya Chen fc2be69469 Merge branch 'main' into issue-7081 2025-01-20 10:44:58 +08:00
Yi-Ya Chen fdc635955b refactor: remove the confirm input
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-17 18:06:04 +08:00
Andy Lee f56f46a543 Merge pull request #97 from a110605/update_year
chore: update copyright year to 2025
2025-01-17 17:27:10 +08:00
Yi-Ya Chen f5961f3a59 fix: check user delete permission
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-17 16:45:27 +08:00
andy.lee 8f3a2b202d chore: update company name to SUSE
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-17 16:17:49 +08:00
andy.lee 0ea3f56f2c chore: update copyright year
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-17 16:00:58 +08:00
andy.lee 6e9350e63d refactor: fallback to longhorn if not default storage class
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-17 14:57:39 +08:00
Andy Lee 1c15ed8003 Merge pull request #92 from a110605/bump_v103_release
chore: bump release version to v1.0.3
2025-01-17 09:57:49 +08:00
Andy Lee 9f3e689727 Merge pull request #91 from a110605/issue_7351
fix: bring default valueContext when customizing vm-import controller
2025-01-17 09:56:58 +08:00
Yi-Ya Chen 526d1bb875 feat: add confirm popup
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-16 17:28:14 +08:00
andy.lee b0c7b1fefe fix: when choose encrypt can only choose encrypted storage class when creating image
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-16 16:18:43 +08:00
andy.lee 6644651fe5 chore: bump release version to v1.0.3
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-16 14:03:46 +08:00
andy.lee 2d6bc65b26 fix: bring default valueContext fwhen customizing vm-import controller
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-16 12:18:57 +08:00
Yi-Ya Chen 3565b44e3a refactor: fix lint warnings
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-15 10:12:38 +08:00
Yi-Ya Chen 43b10b250e refactor: move logic to node model
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-15 10:02:33 +08:00
Francesco Torchia 7b6efa624f Merge pull request #86 from torchiaf/replace-sortablejs-vue3
Replace sortablejs-vue3
2025-01-14 17:02:21 +01:00
Francesco Torchia d9db1263d1 Fix Volumes draggable key
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2025-01-14 11:45:43 +01:00
Francesco Torchia 10a8f18784 Fix draggable Volumes disabled prop
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2025-01-14 10:09:30 +01:00
andy.lee 993706fead fix: remove witness node in VM node scheduling tab
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-14 16:20:20 +08:00
Andy Lee 1098ef8f6a Merge pull request #84 from a110605/issue_7174_2
fix: resume schedule job error
2025-01-14 12:55:41 +08:00
Andy Lee 4480dbdeed Merge pull request #85 from a110605/issue_7347
fix: filter schedule style and functionality
2025-01-14 12:55:24 +08:00
Francesco Torchia a7b4366d7c Replace sortablejs-vue3
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2025-01-13 18:13:18 +01:00
andy.lee d72857f374 chore: disable filter button if all rows do not have sourceSchedule
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-13 16:29:08 +08:00
andy.lee 57ab9e2830 fix: filter schedule style and functionality
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-13 16:07:32 +08:00
andy.lee 8116358011 fix: resume schedule job error
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-13 14:13:00 +08:00
Yi-Ya Chen c791399a61 fix: disable delete action
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-13 14:01:35 +08:00
Andy Lee 5bd8ad2a93 Merge pull request #79 from a110605/fix_7317
fix: add missing wording in delete VM modal
2025-01-13 13:36:52 +08:00
Andy Lee 345fe636ec Merge pull request #72 from torchiaf/7082-host-vms
[porting 1262] Fix vm rows reactivity
2025-01-13 11:58:45 +08:00
andy.lee 7e32ea453b fix: delete VM modal style and wording
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-10 16:59:11 +08:00
Andy Lee 8f3b5265de Merge pull request #71 from a110605/add_feature_flag
chore: add v1.5.0 and v1.3.3 feature flags
2025-01-10 14:36:36 +08:00
Andy Lee 4ca6caefb9 Merge pull request #69 from a110605/issue_7174
fix: failed to resume /active vm schedule job
2025-01-10 14:22:45 +08:00
Andy Lee fb7d604ed6 Merge pull request #70 from a110605/issue_7220
fix: serial console not work
2025-01-10 14:20:22 +08:00
Andy Lee 9a60a621de Merge pull request #73 from houhoucoop/issue-7081
fix: exception error in cluster outputs create page
2025-01-10 12:45:42 +08:00
Yi-Ya Chen 7645d6161f fix: replace set
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-01-10 10:54:58 +08:00
Francesco Torchia c3f2665912 [porting 1262] Fix vm rows reactivity
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
2025-01-09 12:06:35 +01:00
andy.lee 2a58745550 chore: add v1.5.0 and v1.3.3 feature flags
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-09 17:43:19 +08:00
andy.lee 69d7046bc7 fix: serial console not work
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-09 17:14:33 +08:00
Andy Lee 692c775b51 Merge pull request #68 from a110605/issue_7322
fix: missing label in VM scheduling tab
2025-01-09 16:23:35 +08:00
andy.lee 8b43c200b2 fix: remove translation key
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-09 16:13:51 +08:00
andy.lee ab71cb377c fix: update translation key
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-09 10:59:49 +08:00
andy.lee 814d296d75 fix: resume/active scheduling job failed
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-08 15:59:25 +08:00
andy.lee eb667fd56b fix: missing label in VM scheduling tab
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-08 14:41:18 +08:00
andy.lee e4b3f84905 change mergify.yml base branch to main
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 21:37:38 +08:00
Andy Lee ea68b95c26 Merge pull request #67 from a110605/add_mergify
add mergify.yml
2025-01-06 21:34:56 +08:00
andy.lee da77762b7f add mergify.yml
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 21:32:00 +08:00
113 changed files with 6500 additions and 2311 deletions
+6
View File
@@ -0,0 +1,6 @@
addAssignees: author
addReviewers: true
numberOfReviewers: 0
reviewers:
- a110605
- houhoucoop
+13
View File
@@ -12,6 +12,19 @@ pull_request_rules:
labels:
- "v1.5 backport PR"
- 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
+2 -7
View File
@@ -2,18 +2,13 @@
### Summary
### PR Checklists
- Do we need to backport this PR change to the [Harvester Dashboard](https://github.com/harvester/dashboard)?
- [ ] Yes, the relevant PR is at:
- Are backend engineers aware of UI changes?
- Are backend engineers aware of UI changes ?
- [ ] Yes, the backend owner is:
### Related Issue #
<!-- Define findings related to the feature or bug issue. -->
### Test screenshot/video
### Test screenshot or video
<!-- Attach screenshot or video of the changes and eventual comparison if you find it necessary -->
### Extra technical notes summary
<!-- Outline technical changes which may pass unobserved or may help to understand the process of solving the issue -->
+40
View File
@@ -0,0 +1,40 @@
# .github/release.yml
changelog:
categories:
- title: Features
labels:
- feat
- title: Bug Fixes
labels:
- fix
- title: Dependencies
labels:
- deps
- title: Performance
labels:
- perf
- title: Documentation
labels:
- docs
- title: Style
labels:
- style
- title: Build & Continuous Integration
labels:
- build
- ci
- title: Security
labels:
- security
- title: Tests
labels:
- test
- title: Refactoring
labels:
- refactor
- title: Chores
labels:
- chore
- title: Others
labels:
- other
+46
View File
@@ -0,0 +1,46 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":semanticCommitTypeAll(deps)",
":semanticCommitScopeDisabled"
],
"baseBranches": [
"main",
"/^release-harvester-v\\d+\\.\\d+$/"
],
"automergeMajor": false,
"semanticCommits": "enabled",
"semanticCommitType": "deps",
"prHourlyLimit": 12,
"timezone": "Asia/Taipei",
"schedule": ["after 10am on sunday"],
"postUpdateOptions": ["yarnDedupeFewer"],
"packageRules": [
{
"matchUpdateTypes": ["major"],
"enabled": false
},
{
"matchPackagePatterns": ["vue", "vue-router", "vuex"],
"matchUpdateTypes": ["major"],
"enabled": false
},
{
"matchPackageNames": ["@rancher/shell"],
"enabled": false
},
{
"matchUpdateTypes": ["minor"],
"groupName": "minor dependencies",
"labels": ["minor-update"],
"reviewers": ["a110605", "houhoucoop"]
},
{
"matchUpdateTypes": ["patch", "digest"],
"automerge": true,
"groupName": "patch digest dependencies",
"labels": ["patch-update", "automerge"]
}
]
}
@@ -0,0 +1,17 @@
name: "[PR Management] Auto Assign Reviewer & Assignee"
on:
pull_request_target:
types: [opened, ready_for_review]
permissions:
pull-requests: write
jobs:
auto-assign:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: rancher/gh-issue-mgr/auto-assign-action@main
with:
configuration-path: .github/auto-assign-config.yaml
+94
View File
@@ -0,0 +1,94 @@
name: "[PR Management] Add Labels"
on:
pull_request_target:
types: [opened, reopened]
branches:
- main
- 'release-harvester-v*'
permissions:
pull-requests: write
jobs:
add-require-backport-label:
if: github.event.pull_request.draft == false &&
github.event.pull_request.base.ref == 'main'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.base_ref }}
- name: Fetch release branches and PR labels
id: fetch_info
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
repo="${{ github.repository }}"
pr_number=${{ github.event.pull_request.number }}
release_branches=$(gh api "repos/${repo}/branches" --paginate --jq '.[].name' | grep -E '^release-harvester-v[0-9]+\.[0-9]+$' || true)
if [[ -z "$release_branches" ]]; then
echo "should_label=false" >> "$GITHUB_OUTPUT"
exit 0
fi
latest_branch=$(echo "$release_branches" | sort -Vr | head -n1)
version="${latest_branch#release-harvester-v}"
release_tag="v${version}.0"
tags=$(gh api "repos/${repo}/releases" --paginate --jq '.[].tag_name')
if echo "$tags" | grep -Fxq "$release_tag"; then
echo "should_label=false" >> "$GITHUB_OUTPUT"
exit 0
fi
label="require backport/v${version}"
echo "should_label=true" >> "$GITHUB_OUTPUT"
echo "backport_label=$label" >> "$GITHUB_OUTPUT"
pr_labels=$(gh pr view "$pr_number" --repo "$repo" --json labels --jq '.labels[].name' || echo "")
pr_labels_csv=$(echo "$pr_labels" | tr '\n' ',' | sed 's/,$//')
echo "pr_labels=$pr_labels_csv" >> "$GITHUB_OUTPUT"
- name: Add label if needed
if: steps.fetch_info.outputs.should_label == 'true' && !contains(steps.fetch_info.outputs.pr_labels, steps.fetch_info.outputs.backport_label)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Adding label: ${{ steps.fetch_info.outputs.backport_label }}"
gh pr edit ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} \
--add-label "${{ steps.fetch_info.outputs.backport_label }}"
add-backport-label:
if: github.event.pull_request.draft == false &&
startsWith(github.event.pull_request.base.ref, 'release-harvester-v')
runs-on: ubuntu-latest
steps:
- name: Check conditions for backport label
id: check
run: |
IS_MERGIFY=$(echo '${{ github.event.pull_request.user.login }}' | grep -iq 'mergify' && echo true || echo false)
TARGET_BRANCH=${{ github.event.pull_request.base.ref }}
echo "IS_MERGIFY=$IS_MERGIFY" >> $GITHUB_OUTPUT
echo "TARGET_BRANCH=$TARGET_BRANCH" >> $GITHUB_OUTPUT
- name: Add label if needed
if: steps.check.outputs.IS_MERGIFY == 'true' && startsWith(steps.check.outputs.TARGET_BRANCH, 'release-harvester-v')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TARGET_BRANCH="${{ steps.check.outputs.TARGET_BRANCH }}"
version="${TARGET_BRANCH#release-harvester-v}"
label="backport/v${version}"
echo "Adding label $label"
gh pr edit ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} \
--add-label "$label"
@@ -4,6 +4,7 @@ on:
push:
branches:
- 'release-harvester-v*'
- 'main'
jobs:
setup-target-branch:
@@ -17,14 +18,17 @@ jobs:
- name: Determine target branch
id: get-version
run: |
if [[ "${{ github.ref_name }}" =~ ^release-harvester-v([0-9]+\.[0-9]+)$ ]]; then
if [[ "${{ github.ref_name }}" == "main" ]]; then
TARGET_BRANCH="main-head"
elif [[ "${{ github.ref_name }}" =~ ^release-harvester-v([0-9]+\.[0-9]+)$ ]]; then
TARGET_BRANCH="v${BASH_REMATCH[1]}-head"
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_ENV
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_OUTPUT
else
echo "Error: invalid branch format." && exit 1
fi
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_ENV
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_OUTPUT
- name: Ensure target branch exists
run: |
git fetch --all
+30
View File
@@ -0,0 +1,30 @@
name: "[PR Management] Add PR Label"
on:
pull_request_target:
types: [opened, reopened, edited]
branches:
- main
- 'release-harvester-v*'
permissions:
pull-requests: write
jobs:
auto-assign-pr-label:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.base_ref }}
- name: Setup Nodejs and yarn install
uses: ./.github/actions/setup
- name: Set PR label
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
echo "PR_LABEL = $PR_LABEL"
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "$PR_LABEL"
+37 -1
View File
@@ -7,6 +7,7 @@ on:
- main
- 'release-*'
pull_request:
types: [opened, reopened, edited, synchronize]
branches:
- main
- 'release-*'
@@ -17,7 +18,42 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
fetch-depth: 0 # Need full history for commit-lint
- name: Run tests
uses: ./.github/actions/lint
- name: Validate PR title lint
if: github.event_name == 'pull_request'
shell: bash
run: echo "${{ github.event.pull_request.title }}" | npx commitlint
- name: Validate commit messages
shell: bash
run: |
echo "GITHUB_EVENT_NAME=$GITHUB_EVENT_NAME"
echo "GITHUB_BASE_SHA=$GITHUB_BASE_SHA"
echo "GITHUB_HEAD_SHA=$GITHUB_HEAD_SHA"
echo "GITHUB_BEFORE=$GITHUB_BEFORE"
echo "GITHUB_AFTER=$GITHUB_AFTER"
if [ "$GITHUB_EVENT_NAME" = "pull_request" ] && [ -n "$GITHUB_BASE_SHA" ] && [ -n "$GITHUB_HEAD_SHA" ]; then
FROM="$GITHUB_BASE_SHA"
TO="$GITHUB_HEAD_SHA"
elif [ -n "$GITHUB_BEFORE" ] && [ -n "$GITHUB_AFTER" ]; then
FROM="$GITHUB_BEFORE"
TO="$GITHUB_AFTER"
else
echo "No valid commit range found, skipping commitlint."
exit 0
fi
echo "FROM=$FROM"
echo "TO=$TO"
npx commitlint --from "$FROM" --to "$TO" --verbose
env:
GITHUB_EVENT_NAME: ${{ github.event_name }}
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
GITHUB_BEFORE: ${{ github.event.before }}
GITHUB_AFTER: ${{ github.event.after }}
+1
View File
@@ -0,0 +1 @@
npx --no -- commitlint --edit $1
+1
View File
@@ -0,0 +1 @@
yarn lint
+133 -26
View File
@@ -1,62 +1,169 @@
# harvester-ui-extension
Rancher Extension used in [rancher/dashboard](https://github.com/rancher/dashboard) for [Harvester](https://harvesterhci.io) Dashboard UI.
The Harvester UI Extension is a Rancher extension that provides the user interface for [Harvester](https://harvesterhci.io) within the [Rancher Dashboard](https://github.com/rancher/dashboard).
`
From Rancher 2.10.0, Harvester integration is provided by this new Harvester UI Extension.
`
> **Note:**
> This extension is available starting from **Rancher 2.10.0**. Ensure your Rancher version is **2.10.0 or later** to access Harvester integration.
## Installation
To install the Harvester UI Extension in Rancher 2.10.0 and newer, please refer to [installation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210) on harvester doc.
For detailed installation instructions, please refer to the [official Harvester documentation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210).
## Running for Development
This is what you probably want to get started.
### Prerequisite
Nodejs version >= 20
## Development Setup
Ensure **Node.js v20 or later** is installed for development and debugging.
### Standalone Mode
For development, serve with hot reload at https://localhost:8005
Run the extension standalone with hot reload at `https://localhost:8005`.
```bash
# Install dependencies
yarn install
# Use RANCHER_ENV to specify the endpoint for your Harvester API
# Start the development server
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
# Use VUE_APP_SERVER_VERSION to specify the Harvester server version
# in case your are running a dev build of Harvester
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=your-harvester-server-version API=https://your-harvester-ip yarn dev
# Example with specific server version
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.5.0 API=https://192.168.1.123 yarn dev
```
# or put your env variables into a .env file
You may also define environment variables in a `.env` file:
# Example
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.4.0 API=https://192.168.1.123 yarn dev
```env
RANCHER_ENV=harvester
VUE_APP_SERVER_VERSION=v1.5.0
API=https://192.168.1.123
```
### Rancher Integration Mode
In this mode, you need to have Rancher and run Harvester UI Extension as Rancher extension, see more detail in [Rancher UI extension](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
To run as a Rancher extension, follow the [Rancher UI Extension Guide](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
```bash
API=https://your-rancher-ip yarn dev
# Example
API=https://192.168.1.123 yarn dev
```
## Commit Message Guidelines
This project uses [commit-lint](https://commitlint.js.org/) with [Conventional Commits](https://www.conventionalcommits.org/) to ensure consistent and meaningful commit messages.
### Commit Message Format
All commit messages must follow the conventional commit format:
```
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
```
### Supported Types
- **feat**: New features
- **fix**: Bug fixes
- **docs**: Documentation changes
- **style**: Code style changes (formatting, missing semicolons, etc.)
- **refactor**: Code refactoring
- **perf**: Performance improvements
- **test**: Adding or updating tests
- **build**: Build system or external dependencies
- **ci**: CI/CD changes
- **chore**: Other changes that don't modify src or test files
- **revert**: Reverts a previous commit
- **wip**: Work in progress
- **deps**: Dependency updates
- **security**: Security fixes
### Examples
```bash
# Feature
git commit -m "feat: add new virtual machine creation wizard"
# Bug fix
git commit -m "fix: resolve memory leak in VM console"
# Documentation
git commit -m "docs: update installation instructions"
# Breaking change
git commit -m "feat!: change API endpoint structure
BREAKING CHANGE: The /api/v1/vms endpoint has been replaced with /api/v2/vms"
```
### Git Hooks
The project uses [Husky](https://typicode.github.io/husky/) to automatically validate commit messages and run linting before commits:
- **pre-commit**: Runs ESLint to ensure code quality
- **commit-msg**: Validates commit message format using commit-lint
These hooks are automatically installed when you run `yarn install`.
### Manual Validation
You can manually validate commit messages:
```bash
# Validate the last commit
yarn commitlint
# Validate a specific commit
npx commitlint --from <commit-hash>
# Validate a range of commits
npx commitlint --from <start-hash> --to <end-hash>
```
## Branch Structure
- **`main`** – Main development branch
- **`release-harvester-vX.Y`** – Stable release branches per version series
- **`vX.Y-head`** – Testing branches for ongoing changes to extension builds in each release series
> **Note:**
> The `vX.Y-head` branches are auto-generated and kept in sync with release branches. Use these for testing the latest changes in each version series.
## Testing Guidelines
### UI Extension Testing
To validate changes in a release series, switch to the appropriate `vX.Y-head` branch. For main branch testing, use `main-head`.
- 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**
2. Refresh the Harvester repository using the target `vX.Y-head` branch
3. Go to the **Extensions** page and install the desired version
### Standalone Mode Testing
To test the standalone UI, configure Harvester to load the UI from an external source.
- Examples of `ui-index`:
- Main branch → `https://releases.rancher.com/harvester-ui/dashboard/latest/index.html`
- Release series `1.5.x` → `https://releases.rancher.com/harvester-ui/dashboard/release-harvester-v1.5/index.html`
**Steps:**
1. Go to **Harvester UI** → **Advanced** → **Settings** → **UI**
2. Set **ui-source** to `External`
3. Set **ui-index** to the desired URL
## Contributing
For developers, after reading through the introduction on this page, head over to our [Getting Started](https://extensions.rancher.io/extensions/next/extensions-getting-started) guide to learn more.
If you want to contribute, start by reading this document, then visit our [Getting Started guide](https://extensions.rancher.io/extensions/next/extensions-getting-started) to learn how to develop and submit changes.
## License
License
=======
Copyright (c) 2014-2025 [SUSE, LLC.](https://www.suse.com/)
Licensed under the Apache License, Version 2.0 (the "License");
+43
View File
@@ -0,0 +1,43 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
// Enforce conventional commit format
'type-enum': [
2,
'always',
[
'feat', // New features
'fix', // Bug fixes
'docs', // Documentation changes
'style', // Code style changes (formatting, missing semicolons, etc.)
'refactor', // Code refactoring
'perf', // Performance improvements
'test', // Adding or updating tests
'build', // Build system or external dependencies
'ci', // CI/CD changes
'chore', // Other changes that don't modify src or test files
'revert', // Reverts a previous commit
'wip', // Work in progress
'deps', // Dependency updates
'security', // Security fixes
]
],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'subject-case': [0, 'never'],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
'subject-max-length': [0, 'never'],
'body-leading-blank': [2, 'always'],
'body-max-line-length': [2, 'always', 100],
'footer-leading-blank': [2, 'always'],
'footer-max-line-length': [2, 'always', 100],
},
// Ignore merge commits and revert commits
ignores: [
(commit) => commit.includes('Merge'),
(commit) => commit.includes('Revert'),
(commit) => commit.includes('merge'),
(commit) => commit.includes('revert'),
],
};
+13 -7
View File
@@ -1,19 +1,19 @@
{
"name": "harvester-ui-extension",
"version": "1.5.2-rc1",
"version": "1.6.1-rc2",
"private": false,
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@rancher/shell": "3.0.2-rc.6",
"@babel/plugin-transform-class-static-block": "7.28.3",
"@rancher/shell": "3.0.5-rc.8",
"cache-loader": "^4.1.0",
"color": "4.2.3",
"ip": "2.0.1",
"node-polyfill-webpack-plugin": "^3.0.0",
"vue-draggable-next": "^2.2.1",
"yaml": "^2.5.1",
"@babel/plugin-transform-class-static-block": "7.28.3"
"yaml": "^2.5.1"
},
"resolutions": {
"@types/node": "~20.19.0",
@@ -41,10 +41,16 @@
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name"
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
"commitlint": "commitlint --edit",
"prepare": "husky"
},
"devDependencies": {
"@commitlint/load": "^19.8.1",
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^7.1.0"
"eslint-plugin-promise": "^7.1.0",
"husky": "^9.1.7"
}
}
}
+128 -27
View File
@@ -1,8 +1,11 @@
<script>
import { mapGetters } from 'vuex';
import ActionMenu from '@shell/components/ActionMenuShell';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import { HCI_ALLOWED_SETTINGS, HCI_SETTING } from '../config/settings';
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
import { DOC } from '../config/doc-links';
import { docLink } from '../utils/feature-flags';
const CATEGORY = {
ui: [
@@ -19,6 +22,7 @@ export default {
components: {
AsyncButton,
Banner,
ActionMenu
},
props: {
@@ -30,15 +34,23 @@ export default {
category: {
type: String,
required: true,
},
searchQuery: {
type: String,
default: ''
}
},
data() {
const categorySettings = this.filterCategorySettings();
const filteredSettings = this.filterSearchSettings(categorySettings, this.searchQuery);
return {
HCI_SETTING,
categorySettings,
filteredSettings,
originalHideMap: this.createHideMap(categorySettings)
};
},
@@ -48,12 +60,85 @@ export default {
settings: {
deep: true,
handler() {
this['categorySettings'] = this.filterCategorySettings();
this.categorySettings = this.filterCategorySettings();
this.filteredSettings = this.filterSearchSettings(this.categorySettings, this.searchQuery);
}
},
searchQuery: {
immediate: true,
handler(newQuery) {
const filtered = this.filterSearchSettings(this.categorySettings, newQuery);
this.filteredSettings = newQuery ? this.openJsonSettings(filtered) : filtered.map((s) => ({ ...s, hide: this.originalHideMap[s.id] ?? false }));
}
}
},
methods: {
createHideMap(settings = []) {
const map = settings.reduce((acc, s) => {
acc[s.id] = s.hide ?? false;
return acc;
}, {} );
return map;
},
filterSearchSettings(settings, searchKey) {
if (!searchKey) {
return this.filterCategorySettings();
}
const searchQuery = searchKey.toLowerCase();
return settings.filter((setting) => {
const id = setting.id?.toLowerCase() || '';
// filter by id
if (id.includes(searchQuery) ) {
return true;
}
let description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || '';
// remove <a> tags from description
if (description.includes('<a')) {
description = description.replace(/<a[^>]*>(.*?)<\/a>/g, '$1');
}
// filter by description
if (description.includes(searchQuery)) {
return true;
}
// filter by customized value
if (setting.customized === true && setting.data?.value) {
const value = setting.data.value?.toLowerCase() || '';
return value.includes(searchQuery);
}
// filter by json value
if (setting.kind === 'json' && setting.json) {
try {
const json = JSON.parse(setting.json);
const jsonString = JSON.stringify(json).toLowerCase();
return jsonString.includes(searchQuery);
} catch (e) {
console.error(`${ setting.id }: wrong format`, e); // eslint-disable-line no-console
return false;
}
}
// filter by default value
if (setting.data?.default) {
return setting.data?.default.includes(searchQuery);
}
return false;
});
},
filterCategorySettings() {
return this.settings.filter((s) => {
if (!this.getFeatureEnabled(s.featureFlag)) {
@@ -74,25 +159,21 @@ export default {
return id ? this.$store.getters['harvester-common/getFeatureEnabled'](id) : true;
},
showActionMenu(e, setting) {
const actionElement = e.srcElement;
this.$store.commit(`action-menu/show`, {
resources: setting.data,
elem: actionElement
});
},
getSettingOption(id) {
return HCI_ALLOWED_SETTINGS.find((setting) => setting.id === id);
},
openJsonSettings(settings) {
return settings.map((s) => s.hide ? { ...s, hide: false } : s);
},
toggleHide(s) {
this.categorySettings.find((setting) => {
if (setting.id === s.id) {
setting.hide = !setting.hide;
}
});
const setting = this.filteredSettings.find((setting) => setting.id === s.id);
if (setting) {
setting.hide = !setting.hide;
this.originalHideMap[setting.id] = setting.hide;
}
},
async testConnect(buttonDone, value) {
@@ -118,6 +199,19 @@ export default {
}
buttonDone(false);
}
},
getDocLinkParams(setting) {
const settingConfig = HCI_ALLOWED_SETTINGS[setting.id] || HCI_SINGLE_CLUSTER_ALLOWED_SETTING[setting.id];
if (settingConfig?.docPath) {
const version = this.$store.getters['harvester-common/getServerVersion']();
const url = docLink(DOC[settingConfig.docPath], version);
return { url };
}
return {};
}
},
};
@@ -126,7 +220,7 @@ export default {
<template>
<div>
<div
v-for="(setting, i) in categorySettings"
v-for="(setting, i) in filteredSettings"
:key="i"
class="advanced-setting mb-20"
>
@@ -148,7 +242,7 @@ export default {
Experimental
</span>
</h1>
<h2 v-clean-html="t(setting.description, {}, true)">
<h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)">
</h2>
</div>
<div
@@ -156,15 +250,12 @@ export default {
:id="setting.id"
class="action"
>
<button
aria-haspopup="true"
aria-expanded="false"
type="button"
class="btn btn-sm role-multi-action actions"
@click="showActionMenu($event, setting)"
>
<i class="icon icon-actions" />
</button>
<ActionMenu
:resource="setting.data"
:button-aria-label="t('advancedSettings.edit.label')"
data-testid="action-button"
button-role="tertiary"
/>
</div>
</div>
<div value>
@@ -221,6 +312,12 @@ export default {
{{ setting.data.errMessage }}
</Banner>
</div>
<div
v-if="filteredSettings.length === 0"
class="advanced-setting mb-20 no-search-match"
>
<p> {{ t('harvester.setting.noSearchMatch') }} </p>
</div>
</div>
</template>
@@ -271,4 +368,8 @@ export default {
padding: 2px 10px;
font-size: 12px;
}
.no-search-match {
text-align: center;
}
</style>
@@ -258,7 +258,7 @@ export default {
},
softReboot() {
this.vmResource.softrebootVM();
this.vmResource.doSoftReboot();
},
showKeysModal() {
@@ -372,3 +372,12 @@ export default {
background: rgb(40, 40, 40);
}
</style>
<style lang="scss">
.vm-console .v-popper__arrow-container {
display: none;
}
.vm-console .v-popper__popper{
margin-top: 8px;
}
</style>
@@ -0,0 +1,294 @@
<script>
import { _EDIT } from '@shell/config/query-params';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import InfoBox from '@shell/components/InfoBox';
import { Banner } from '@components/Banner';
import { allHash } from '@shell/utils/promise';
import { CSI_DRIVER } from '../../types';
import { LONGHORN_DRIVER } from '@shell/config/types';
export default {
name: 'CSIOnlineExpandValidation',
components: {
Banner,
InfoBox,
LabeledSelect,
},
props: {
mode: {
type: String,
default: _EDIT,
},
value: {
type: Object,
default: () => ({}),
},
registerBeforeHook: {
type: Function,
required: true,
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
try {
await allHash({ csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }) });
this.fetchError = null;
} catch (error) {
console.error('Failed to fetch CSI drivers:', error); // eslint-disable-line no-console
this.fetchError = this.t(
'harvester.setting.csiOnlineExpandValidation.failedToLoadDrivers',
{ error: error.message || error },
true
);
}
},
data() {
return {
configArr: [],
parseError: null,
fetchError: null,
};
},
created() {
const initValue = this.value.value || this.value.default || '{}';
this.configArr = this.parseValue(initValue);
this.registerBeforeHook?.(this.willSave, 'willSave');
},
computed: {
allErrors() {
const errors = [];
if (this.fetchError) {
errors.push(this.fetchError);
}
if (this.parseError) {
errors.push(this.parseError);
}
return errors;
},
csiDrivers() {
if (this.fetchError) return [];
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || [];
},
provisioners() {
const usedKeys = this.configArr.map(({ key }) => key);
return this.csiDrivers
.filter(({ name }) => !usedKeys.includes(name))
.map(({ name }) => name);
},
provisionerValue() {
return [
{ label: 'True', value: true },
{ label: 'False', value: false },
];
},
disableAdd() {
return this.parseError || this.fetchError || this.configArr.length >= this.csiDrivers.length;
},
disableConfigEditing() {
return this.parseError || this.fetchError;
}
},
watch: {
'value.value'(newVal, oldVal) {
if (newVal !== oldVal) {
this.configArr = this.parseValue(newVal || '{}');
}
}
},
methods: {
_convertToBoolean(value) {
if (typeof value === 'boolean') return value;
if (typeof value === 'string') {
const lowerCaseValue = value.toLowerCase();
if (lowerCaseValue === 'true') return true;
if (lowerCaseValue === 'false') return false;
}
return false; // default to false for any other string or non-boolean type
},
parseValue(raw) {
try {
const json = JSON.parse(raw);
this.parseError = null;
return Object.entries(json).map(([key, value]) => ({
key,
value: this._convertToBoolean(value),
}));
} catch (e) {
console.error('[CSIOnlineExpandValidation] JSON Parsing Error:', raw, e); // eslint-disable-line no-console
this.parseError = this.t(
'harvester.setting.csiOnlineExpandValidation.invalidJsonFormat',
{ error: e.message },
true
);
return [];
}
},
stringifyConfig() {
const obj = {};
this.configArr.forEach(({ key, value }) => {
obj[key] = value;
});
return this.configArr.length ? JSON.stringify(obj) : '';
},
update() {
this.value.value = this.stringifyConfig();
},
willSave() {
const errors = [];
this.configArr.forEach(({ key }) => {
if (!key) {
errors.push(
this.t('validation.required', { key: this.t('harvester.setting.csiOnlineExpandValidation.provisioner') }, true)
);
}
});
this.value.value = this.stringifyConfig();
return errors.length ? Promise.reject(errors) : Promise.resolve();
},
useDefault() {
this.configArr = this.parseValue(this.value.default || '{}');
this.update();
},
disableEdit(driverKey) {
return this.fetchError || driverKey === LONGHORN_DRIVER;
},
add() {
if (this.disableConfigEditing) return;
this.configArr.push({ key: '', value: true });
},
remove(index) {
if (this.disableConfigEditing) return;
this.configArr.splice(index, 1);
this.update();
},
onValueChange(idx, newVal) {
if (this.disableConfigEditing) return;
const val = newVal === 'true' ? true : newVal === 'false' ? false : newVal;
this.configArr[idx].value = val;
this.update();
},
},
};
</script>
<template>
<div>
<Banner
v-for="(errorMsg, index) in allErrors"
:key="index"
color="error"
>
{{ errorMsg }}
</Banner>
<InfoBox
v-for="(driver, idx) in configArr"
:key="idx"
class="box"
>
<button
class="role-link btn btn-sm remove"
type="button"
:disabled="disableEdit(driver.key)"
@click="remove(idx)"
>
<i class="icon icon-x" />
</button>
<div class="row">
<div class="col span-4">
<LabeledSelect
v-model:value="driver.key"
label-key="harvester.setting.csiOnlineExpandValidation.provisioner"
required
searchable
:mode="mode"
:disabled="disableEdit(driver.key)"
:options="provisioners"
@update:value="update"
@keydown.native.enter.prevent
/>
</div>
<div class="col span-4">
<LabeledSelect
v-model:value="driver.value"
:value="driver.value.toString()"
label-key="harvester.setting.csiOnlineExpandValidation.value"
required
searchable
:mode="mode"
:disabled="disableEdit(driver.key)"
:options="provisionerValue"
@update:value="val => onValueChange(idx, val)"
@keydown.native.enter.prevent
/>
</div>
</div>
</InfoBox>
<button
class="btn btn-sm role-primary"
:disabled="disableAdd"
@click="add"
>
{{ t('generic.add') }}
</button>
</div>
</template>
<style lang="scss" scoped>
.box {
position: relative;
padding-top: 40px;
}
.remove {
position: absolute;
top: 10px;
right: 10px;
padding: 0;
}
</style>
@@ -0,0 +1,222 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import { RadioGroup } from '@components/Form/Radio';
import { SECRET } from '@shell/config/types';
import { exceptionToErrorsArray } from '@shell/utils/error';
import FileSelector, { createOnSelected } from '@shell/components/form/FileSelector';
import YamlEditor from '@shell/components/YamlEditor';
export default {
name: 'HarvesterRancherCluster',
components: {
RadioGroup,
FileSelector,
YamlEditor
},
mixins: [CreateEditView],
props: {
value: {
type: Object,
default: () => ({}),
},
registerBeforeHook: {
type: Function,
required: false,
default: () => {},
},
},
data() {
let parseDefaultValue = {};
try {
const data = this.value.value || this.value.default || '{}';
const parsed = JSON.parse(data);
parseDefaultValue = {
kubeConfig: '',
removeUpstreamClusterWhenNamespaceIsDeleted: parsed.removeUpstreamClusterWhenNamespaceIsDeleted || false
};
} catch (error) {
parseDefaultValue = {
kubeConfig: '',
removeUpstreamClusterWhenNamespaceIsDeleted: false
};
}
return {
parseDefaultValue,
errors: [],
existingSecret: null,
};
},
async created() {
await this.checkExistingSecret();
if (this.registerBeforeHook) {
this.registerBeforeHook(this.willSave, 'willSave');
}
},
methods: {
onKeySelected: createOnSelected('parseDefaultValue.kubeConfig'),
update() {
if (this.parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted) {
this.value['value'] = JSON.stringify({ removeUpstreamClusterWhenNamespaceIsDeleted: true });
} else {
this.value['value'] = this.value.default || '{}';
}
},
async checkExistingSecret() {
const inStore = this.$store.getters['currentProduct'].inStore;
await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
const secrets = this.$store.getters[`${ inStore }/all`](SECRET) || [];
this.existingSecret = secrets.find((secret) => secret.metadata.name === 'rancher-cluster-config' &&
secret.metadata.namespace === 'harvester-system'
);
// If the secret exists and has data, populate the kubeConfig
if (this.existingSecret?.data?.kubeConfig) {
const decodedContent = atob(this.existingSecret.data.kubeConfig);
this.parseDefaultValue.kubeConfig = decodedContent;
this.$nextTick(() => {
this.update();
});
}
},
async createOrUpdateRancherKubeConfigSecret() {
this.errors = [];
// Check if kubeConfig is provided
if (!this.parseDefaultValue.kubeConfig) {
this.errors.push(this.t('validation.required', { key: this.t('harvester.setting.rancherCluster.kubeConfig') }, true));
return Promise.reject(this.errors);
}
try {
let secret;
if (this.existingSecret) {
secret = this.existingSecret;
secret.setData('kubeConfig', this.parseDefaultValue.kubeConfig);
} else {
const inStore = this.$store.getters['currentProduct'].inStore;
secret = await this.$store.dispatch(`${ inStore }/create`, {
apiVersion: 'v1',
kind: 'Secret',
metadata: {
name: 'rancher-cluster-config',
namespace: 'harvester-system'
},
type: 'secret',
data: { kubeConfig: btoa(this.parseDefaultValue.kubeConfig) }
});
}
await secret.save();
return Promise.resolve();
} catch (err) {
this.errors = exceptionToErrorsArray(err);
return Promise.reject(this.errors);
}
},
async deleteRancherKubeConfigSecret() {
if (this.existingSecret) {
this.existingSecret.remove();
}
},
async willSave() {
// Only create or update secret if enabled
if (this.parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted) {
await this.createOrUpdateRancherKubeConfigSecret();
} else {
await this.deleteRancherKubeConfigSecret();
}
return Promise.resolve();
},
useDefault() {
this.parseDefaultValue = {
kubeConfig: '',
removeUpstreamClusterWhenNamespaceIsDeleted: false
};
}
},
watch: {
'parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted'(val, oldVal) {
if (val && !oldVal && this.existingSecret?.data?.kubeConfig) {
// Populate kubeConfig with the existing secret value
this.parseDefaultValue.kubeConfig = atob(this.existingSecret.data.kubeConfig);
}
},
'parseDefaultValue.kubeConfig'(val) {
this.$refs.yaml?.updateValue(val);
}
}
};
</script>
<template>
<div>
<div class="row mt-20">
<div class="col span-12">
<RadioGroup
v-model:value="parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted"
:label="t('harvester.setting.rancherCluster.removeUpstreamClusterWhenNamespaceIsDeleted')"
name="removeUpstreamClusterWhenNamespaceIsDeleted"
:options="[true, false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
@update:value="update"
/>
</div>
</div>
<div
v-if="parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted"
class="row mt-20"
>
<div class="col span-12">
<FileSelector
class="btn btn-sm bg-primary mb-10"
:label="t('generic.readFromFile')"
@selected="onKeySelected"
/>
<YamlEditor
ref="yaml"
v-model:value="parseDefaultValue.kubeConfig"
class="yaml-editor"
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
@update:value="update"
/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
$yaml-height: 540px;
:deep() .yaml-editor{
flex: 1;
min-height: $yaml-height;
& .code-mirror .CodeMirror {
position: initial;
height: auto;
min-height: $yaml-height;
}
}
</style>
@@ -3,6 +3,9 @@ import { NAMESPACE } from '@shell/config/types';
import CreateEditView from '@shell/mixins/create-edit-view';
import LabeledSelect from '@shell/components/form/LabeledSelect';
const SELECT_ALL = 'select_all';
const UNSELECT_ALL = 'unselect_all';
export default {
name: 'HarvesterBundleNamespaces',
@@ -11,47 +14,74 @@ export default {
mixins: [CreateEditView],
async fetch() {
this.loading = true;
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
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 = [];
} finally {
this.loading = false;
}
},
data() {
let namespaces = [];
const namespacesStr = this.value?.value || this.value?.default || '';
const namespaces = namespacesStr ? namespacesStr.split(',') : [];
if (namespacesStr) {
namespaces = namespacesStr.split(',');
}
return { namespaces };
return {
namespaces,
defaultNamespaces: [],
loading: true
};
},
computed: {
allNamespaces() {
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
},
filteredNamespaces() {
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
},
namespaceOptions() {
return this.$store.getters['harvester/all'](NAMESPACE).map((N) => {
return {
label: N.id,
value: N.id
};
});
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, ...this.filteredNamespaces];
}
},
methods: {
update() {
const namespaceStr = this.namespaces.join(',');
update(selected) {
if (selected.includes(SELECT_ALL)) {
this.namespaces = [...this.filteredNamespaces];
} else if (selected.includes(UNSELECT_ALL)) {
this.namespaces = [];
} else {
this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL);
}
this.value['value'] = namespaceStr;
this.value.value = this.namespaces.join(',');
}
},
watch: {
'value.value': {
handler(neu) {
if (neu === this.value.default || !neu) {
this.namespaces = [];
}
},
deep: true
'value.value'(newVal) {
const raw = newVal || this.value.default || '';
this.namespaces = raw ? raw.split(',') : [];
}
}
};
@@ -62,6 +92,7 @@ export default {
<div class="col span-12">
<LabeledSelect
v-model:value="namespaces"
:loading="loading"
:multiple="true"
label-key="nameNsDescription.namespace.label"
:mode="mode"
@@ -0,0 +1,163 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { RadioGroup } from '@components/Form/Radio';
import { mapGetters } from 'vuex';
export default {
name: 'HarvesterUpgradeConfig',
components: {
LabeledInput,
LabeledSelect,
RadioGroup
},
mixins: [CreateEditView],
data() {
let parseDefaultValue = {};
try {
parseDefaultValue = this.value.value ? JSON.parse(this.value.value) : JSON.parse(this.value.default);
} catch (error) {
parseDefaultValue = JSON.parse(this.value.default);
}
parseDefaultValue = this.normalizeValue(parseDefaultValue);
return {
parseDefaultValue,
errors: []
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
strategyOptions() {
return [
{ value: 'sequential', label: 'sequential' },
{ value: 'skip', label: 'skip' },
{ value: 'parallel', label: 'parallel' }
];
}
},
created() {
this.update();
},
methods: {
normalizeValue(obj) {
if (!obj.imagePreloadOption) {
obj.imagePreloadOption = { strategy: { type: 'sequential' } };
}
if (!obj.imagePreloadOption.strategy) {
obj.imagePreloadOption.strategy = { type: 'sequential' };
}
if (!obj.imagePreloadOption.strategy.type) {
obj.imagePreloadOption.strategy.type = 'sequential';
}
// Only set concurrency if type is 'parallel'
if (obj.imagePreloadOption.strategy.type === 'parallel') {
if (typeof obj.imagePreloadOption.strategy.concurrency !== 'number') {
obj.imagePreloadOption.strategy.concurrency = 0;
}
} else {
delete obj.imagePreloadOption.strategy.concurrency;
}
if (typeof obj.restoreVM !== 'boolean') {
obj.restoreVM = false;
}
return obj;
},
update() {
try {
// Clone to avoid mutating the form state
const valueToSave = JSON.parse(JSON.stringify(this.parseDefaultValue));
if (valueToSave.imagePreloadOption && valueToSave.imagePreloadOption.strategy) {
if (valueToSave.imagePreloadOption.strategy.type !== 'parallel') {
delete valueToSave.imagePreloadOption.strategy.concurrency;
}
}
this.value['value'] = JSON.stringify(valueToSave, null, 2);
this.errors = [];
} catch (e) {
this.errors = ['Invalid JSON'];
}
}
},
watch: {
value: {
handler(neu) {
let parseDefaultValue;
try {
parseDefaultValue = JSON.parse(neu.value);
} catch (err) {
parseDefaultValue = JSON.parse(this.value.default);
}
parseDefaultValue = this.normalizeValue(parseDefaultValue);
this['parseDefaultValue'] = parseDefaultValue;
this.update();
},
deep: true
}
}
};
</script>
<template>
<div>
<div
class="row"
@input="update"
>
<div class="col span-12">
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.imagePreloadStrategy') }}</b></label>
<LabeledSelect
v-model:value="parseDefaultValue.imagePreloadOption.strategy.type"
class="mb-20"
:mode="mode"
:label="t('harvester.setting.upgrade.strategyType')"
:options="strategyOptions"
@update:value="update"
/>
<LabeledInput
v-if="parseDefaultValue.imagePreloadOption.strategy.type === 'parallel'"
v-model:value.number="parseDefaultValue.imagePreloadOption.strategy.concurrency"
class="mb-20"
:mode="mode"
:label="t('harvester.setting.upgrade.concurrency')"
min="0"
type="number"
/>
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.restoreVM') }}</b></label>
<RadioGroup
v-model:value="parseDefaultValue.restoreVM"
class="mb-20"
name="restoreVM"
:options="[true, false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
@update:value="update"
/>
<div
v-if="errors.length"
class="error"
>
{{ errors[0] }}
</div>
</div>
</div>
</div>
</template>
<style scoped>
.error {
color: #d9534f;
margin-top: 5px;
}
</style>
@@ -0,0 +1,270 @@
<script>
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
import { RadioGroup } from '@components/Form/Radio';
import { Banner } from '@components/Banner';
import ArrayList from '@shell/components/form/ArrayList';
import { allHash } from '@shell/utils/promise';
import { isValidCIDR } from '@shell/utils/validators/cidr';
import { NODE } from '@shell/config/types';
import { _EDIT } from '@shell/config/query-params';
import { HCI } from '../../types';
const DEFAULT_NETWORK = {
clusterNetwork: '',
vlan: '',
range: '',
exclude: [],
};
export default {
name: 'VMMigrationNetwork',
components: {
LabeledInput,
LabeledSelect,
RadioGroup,
Banner,
ArrayList,
},
props: {
registerBeforeHook: {
type: Function,
required: true,
},
mode: {
type: String,
default: _EDIT,
},
value: {
type: Object,
default: () => ({ value: '' }),
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
try {
await allHash({
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
});
this.fetchError = null;
} catch (e) {
console.error('Failed to fetch network data:', e); // eslint-disable-line no-console
this.fetchError = this.t('harvester.setting.vmMigrationNetwork.fetchError', { error: e.message || e }, true);
}
},
data() {
const { parsed, enabled, parseError } = this.parseInitialValue();
return {
enabled,
network: { ...DEFAULT_NETWORK, ...parsed },
fetchError: null,
parseError,
};
},
created() {
this.registerBeforeHook?.(this.willSave, 'willSave');
},
computed: {
allErrors() {
return [this.fetchError, this.parseError].filter(Boolean);
},
clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const networks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
return networks.map((net) => ({
label: net.isReadyForStorageNetwork ? net.id : `${ net.id } (${ this.t('generic.notReady') })`,
value: net.id,
disabled: !net.isReadyForStorageNetwork,
}));
},
disableEdit() {
return !!(this.fetchError || this.parseError);
},
},
methods: {
parseInitialValue() {
let parsed = {};
let enabled = false;
let parseError = null;
try {
if (typeof this.value.value === 'string' && this.value.value.trim()) {
parsed = JSON.parse(this.value.value);
enabled = true;
}
} catch (e) {
console.error('[VMMigrationNetwork] Failed to parse value:', e); // eslint-disable-line no-console
parseError = this.t('harvester.setting.vmMigrationNetwork.parseError', { error: e.message }, true);
}
if (!Array.isArray(parsed.exclude)) {
parsed.exclude = [];
}
return {
parsed, enabled, parseError
};
},
clearErrors() {
this.fetchError = null;
this.parseError = null;
},
inputVlan(val) {
this.network.vlan = val ? Math.min(4094, Math.max(1, Number(val))) : '';
this.update();
},
useDefault() {
this.network = { ...DEFAULT_NETWORK };
this.value.value = '';
this.enabled = false;
this.clearErrors();
},
update() {
try {
this.value.value = this.enabled ? JSON.stringify({
...this.network,
exclude: (this.network.exclude || []).filter((e) => !!e?.trim()),
}) : '';
} catch (e) {
console.error('Failed to stringify network config:', e); // eslint-disable-line no-console
this.value.value = '';
}
},
validateInputs() {
const errors = [];
if (!this.network.clusterNetwork) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.clusterNetwork') }, true));
}
if (!this.network.range) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.range.label') }, true));
} else if (!isValidCIDR(this.network.range)) {
errors.push(this.t('harvester.setting.vmMigrationNetwork.range.invalid', null, true));
}
if (this.network.vlan === '') {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.vlan') }, true));
} else {
const vlan = Number(this.network.vlan);
if (isNaN(vlan) || vlan < 1 || vlan > 4094) {
errors.push(this.t('validation.between', {
key: this.t('harvester.setting.vmMigrationNetwork.vlan'),
min: 1,
max: 4094,
}, true));
}
}
for (const cidr of this.network.exclude || []) {
if (cidr && !isValidCIDR(cidr)) {
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', { value: cidr }, true));
}
}
return errors;
},
async willSave() {
if (!this.enabled) {
this.useDefault();
return Promise.resolve();
}
this.update();
const errors = this.validateInputs();
return errors.length ? Promise.reject(errors) : Promise.resolve();
},
},
};
</script>
<template>
<div>
<Banner
v-for="(errorMsg, index) in allErrors"
:key="index"
color="error"
>
{{ errorMsg }}
</Banner>
<RadioGroup
v-model:value="enabled"
class="mb-20"
name="enableMigrationNetwork"
:options="[true, false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
@update:value="update"
/>
<template v-if="enabled">
<LabeledSelect
v-model:value="network.clusterNetwork"
required
label-key="harvester.setting.vmMigrationNetwork.clusterNetwork"
class="mb-20"
:mode="mode"
:options="clusterNetworkOptions"
:disabled="disableEdit"
@update:value="update"
/>
<LabeledInput
v-model:value.number="network.vlan"
required
type="number"
class="mb-20"
:min="1"
:max="4094"
:mode="mode"
placeholder="e.g. 1 - 4094"
label-key="harvester.setting.vmMigrationNetwork.vlan"
:disabled="disableEdit"
@update:value="inputVlan"
/>
<LabeledInput
v-model:value="network.range"
required
class="mb-5"
:mode="mode"
:placeholder="t('harvester.setting.vmMigrationNetwork.range.placeholder')"
label-key="harvester.setting.vmMigrationNetwork.range.label"
:disabled="disableEdit"
@update:value="update"
/>
<ArrayList
v-model:value="network.exclude"
:show-header="true"
:default-add-value="''"
:mode="mode"
:add-disabled="disableEdit"
:add-label="t('harvester.setting.vmMigrationNetwork.exclude.addButton')"
:value-label="t('harvester.setting.vmMigrationNetwork.exclude.label')"
:value-placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
@update:value="update"
/>
</template>
</div>
</template>
+7 -5
View File
@@ -1,8 +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`,
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
UPGRADE_URL: `/upgrade/index`
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`,
};
+15 -1
View File
@@ -21,6 +21,7 @@ const FEATURE_FLAGS = {
'vmSnapshotQuota',
'longhornV2LVMSupport',
'improveMaintenanceMode',
'upgradeConfigSetting'
],
'v1.4.1': [],
'v1.4.2': [
@@ -33,9 +34,22 @@ const FEATURE_FLAGS = {
'efiPersistentState',
'untaggedNetworkSetting',
'skipSingleReplicaDetachedVol',
'thirdPartyStorage'
'thirdPartyStorage',
'liveMigrationProgress'
],
'v1.5.1': [],
'v1.6.0': [
'vmMachineTypes',
'customSupportBundle',
'csiOnlineExpandValidation',
'vmNetworkMigration',
'kubeovnVpcSubnet',
'rancherClusterSetting',
'cpuMemoryHotplug',
'cdiSettings',
'vmCloneRunStrategy',
],
'v1.6.1': [],
};
const generateFeatureFlags = () => {
+21 -2
View File
@@ -24,6 +24,7 @@ import {
CONFIGURED_PROVIDERS,
SUB_TYPE,
ADDRESS,
DESCRIPTION,
} from '@shell/config/table-headers';
import { IF_HAVE } from '@shell/store/type-map';
import {
@@ -32,7 +33,8 @@ import {
IMAGE_PROGRESS,
SNAPSHOT_TARGET_VOLUME,
IMAGE_VIRTUAL_SIZE,
IMAGE_STORAGE_CLASS
IMAGE_STORAGE_CLASS,
HARVESTER_DESCRIPTION
} from './table-headers';
const TEMPLATE = HCI.VM_VERSION;
@@ -222,6 +224,7 @@ export function init($plugin, store) {
STATE,
NAME_COL,
NAMESPACE_COL,
HARVESTER_DESCRIPTION,
IMAGE_STORAGE_CLASS,
IMAGE_PROGRESS,
IMAGE_DOWNLOAD_SIZE,
@@ -255,7 +258,7 @@ export function init($plugin, store) {
});
if (isSingleVirtualCluster) {
headers(NAMESPACE, [STATE, NAME_UNLINKED, AGE]);
headers(NAMESPACE, [STATE, NAME_UNLINKED, DESCRIPTION, AGE]);
basicType([NAMESPACE]);
virtualType({
labelKey: 'harvester.namespace.label',
@@ -421,6 +424,7 @@ export function init($plugin, store) {
[
HCI.CLUSTER_NETWORK,
HCI.NETWORK_ATTACHMENT,
HCI.VPC,
HCI.LB,
HCI.IP_POOL,
],
@@ -547,6 +551,21 @@ export function init($plugin, store) {
exact: false
});
configureType(HCI.VPC, { hiddenNamespaceGroupButton: true, canYaml: false });
virtualType({
labelKey: 'harvester.vpc.label',
name: HCI.VPC,
namespaced: true,
weight: 187,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VPC }
},
exact: false,
ifHaveType: HCI.VPC,
});
configureType(HCI.SNAPSHOT, {
isCreatable: false,
location: {
+14
View File
@@ -1,3 +1,5 @@
import { HCI } from '../types';
// TODO: delete this not used variable
export const MemoryUnit = [{
label: 'Mi',
@@ -70,6 +72,7 @@ export const ADD_ONS = {
RANCHER_MONITORING: 'rancher-monitoring',
VM_IMPORT_CONTROLLER: 'vm-import-controller',
LVM_DRIVER: 'lvm.driver.harvesterhci.io',
KUBEOVN_OPERATOR: 'kubeovn-operator',
};
export const CSI_SECRETS = {
@@ -80,3 +83,14 @@ export const CSI_SECRETS = {
CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name',
CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace',
};
// Some harvester CRD type is not equal to model file name, define the mapping here
export const HARVESTER_CRD_MAP = {
node: HCI.HOST,
configmap: HCI.CLOUD_TEMPLATE,
persistentvolumeclaim: HCI.VOLUME,
'snapshot.storage.k8s.io.volumesnapshot': HCI.SNAPSHOT,
// specific groupable table detail page
'network.harvesterhci.io.vlanconfig': HCI.CLUSTER_NETWORK,
'kubeovn.io.subnet': HCI.VPC
};
+10 -1
View File
@@ -35,6 +35,7 @@ export const HCI = {
NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
NETWORK_ROUTE: 'network.harvesterhci.io/route',
MATCHED_NODES: 'network.harvesterhci.io/matched-nodes',
UPGRADE: 'harvesterhci.io/upgrade',
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
UPGRADE_STATE: 'harvesterhci.io/upgradeState',
@@ -66,5 +67,13 @@ export const HCI = {
VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails',
SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId',
DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine',
K8S_ARCH: 'kubernetes.io/arch'
K8S_ARCH: 'kubernetes.io/arch',
IMAGE_DISPLAY_NAME: 'harvesterhci.io/imageDisplayName',
CUSTOM_IP: 'harvesterhci.io/custom-ip',
IMPORTED_IMAGE: 'migration.harvesterhci.io/imported',
VM_CPU_MEMORY_HOTPLUG: 'harvesterhci.io/enableCPUAndMemoryHotplug',
FILESYSTEM_OVERHEAD: 'cdi.harvesterhci.io/filesystemOverhead',
CLONE_STRATEGY: 'cdi.harvesterhci.io/storageProfileCloneStrategy',
VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes',
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
};
+1
View File
@@ -1 +1,2 @@
export const CLUSTER_NETWORK = 'clusterNetwork';
export const VPC = 'vpc';
+31 -10
View File
@@ -28,12 +28,17 @@ export const HCI_SETTING = {
RELEASE_DOWNLOAD_URL: 'release-download-url',
CCM_CSI_VERSION: 'harvester-csi-ccm-versions',
CSI_DRIVER_CONFIG: 'csi-driver-config',
CSI_ONLINE_EXPAND_VALIDATION: 'csi-online-expand-validation',
VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds',
NTP_SERVERS: 'ntp-servers',
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
UPGRADE_CONFIG: 'upgrade-config',
VM_MIGRATION_NETWORK: 'vm-migration-network',
RANCHER_CLUSTER: 'rancher-cluster',
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio'
};
export const HCI_ALLOWED_SETTINGS = {
@@ -52,18 +57,21 @@ export const HCI_ALLOWED_SETTINGS = {
from: 'import',
featureFlag: 'autoRotateRke2CertsSetting'
},
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
[HCI_SETTING.ADDITIONAL_CA]: {
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
[HCI_SETTING.CSI_ONLINE_EXPAND_VALIDATION]: {
kind: 'json', from: 'import', featureFlag: 'csiOnlineExpandValidation'
},
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
[HCI_SETTING.ADDITIONAL_CA]: {
kind: 'multiline', canReset: true, from: 'import'
},
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: {},
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: {},
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: { kind: 'number' },
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: { kind: 'number' },
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { kind: 'number', featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' },
[HCI_SETTING.STORAGE_NETWORK]: {
kind: 'custom', from: 'import', canReset: true
@@ -89,13 +97,26 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.NTP_SERVERS]: {
kind: 'json', from: 'import', canReset: true
},
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { kind: 'number', featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: {
kind: 'boolean',
experimental: true,
featureFlag: 'longhornV2LVMSupport'
},
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
[HCI_SETTING.UPGRADE_CONFIG]: {
kind: 'json',
from: 'import',
featureFlag: 'upgradeConfigSetting',
docPath: 'UPGRADE_CONFIG_URL'
},
[HCI_SETTING.RANCHER_CLUSTER]: {
kind: 'custom', from: 'import', canReset: true, featureFlag: 'rancherClusterSetting'
},
[HCI_SETTING.MAX_HOTPLUG_RATIO]: { kind: 'number', featureFlag: 'cpuMemoryHotplug' },
[HCI_SETTING.VM_MIGRATION_NETWORK]: {
kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration',
},
};
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
+34 -1
View File
@@ -1,6 +1,7 @@
/**
* Harvester
*/
import { DESCRIPTION } from '@shell/config/table-headers';
// image
export const IMAGE_DOWNLOAD_SIZE = {
@@ -96,5 +97,37 @@ export const IMAGE_STORAGE_CLASS = {
sort: 'imageStorageClass',
value: 'imageStorageClass',
align: 'left',
width: 200,
width: 150,
};
export const HARVESTER_DESCRIPTION = {
...DESCRIPTION,
width: 150,
};
// The CIDR_BLOCK column in VPC list page
export const CIDR_BLOCK = {
name: 'cidrBlock',
labelKey: 'harvester.subnet.cidrBlock.label',
sort: 'cidrBlock',
value: 'spec.cidrBlock',
align: 'left',
};
// The Protocol column in VPC list page
export const PROTOCOL = {
name: 'protocol',
labelKey: 'harvester.subnet.protocol.label',
sort: 'protocol',
value: 'spec.protocol',
align: 'left',
};
// The Provider column in VPC list page
export const PROVIDER = {
name: 'provider',
labelKey: 'harvester.subnet.provider.label',
sort: 'provider',
value: 'spec.provider',
align: 'left',
};
+12 -1
View File
@@ -5,10 +5,21 @@ export const BACKUP_TYPE = {
export const NETWORK_TYPE = {
L2VLAN: 'L2VlanNetwork',
UNTAGGED: 'UntaggedNetwork'
UNTAGGED: 'UntaggedNetwork',
OVERLAY: 'OverlayNetwork',
};
export const VOLUME_MODE = {
BLOCK: 'Block',
FILE_SYSTEM: 'Filesystem'
};
export const NETWORK_PROTOCOL = {
IPv4: 'IPv4',
IPv6: 'IPv6',
};
export const INTERNAL_STORAGE_CLASS = {
VMSTATE_PERSISTENCE: 'vmstate-persistence',
LONGHORN_STATIC: 'longhorn-static',
};
@@ -47,20 +47,26 @@ export default {
STATE,
NAME,
{
name: 'vmCPU',
labelKey: 'tableHeaders.cpu',
search: false,
sort: ['spec.template.spec.domain.cpu.cores'],
value: 'spec.template.spec.domain.cpu.cores',
width: 120
name: 'CPU',
label: 'CPU',
sort: ['displayCPU'],
value: 'displayCPU',
align: 'center',
dashIfEmpty: true,
},
{
name: 'vmRAM',
labelKey: 'glance.memory',
search: false,
sort: ['memorySort'],
value: 'spec.template.spec.domain.resources.limits.memory',
width: 120
name: 'Memory',
value: 'displayMemory',
sort: ['memorySort'],
align: 'center',
labelKey: 'tableHeaders.memory',
formatter: 'Si',
formatterOpts: {
opts: {
increment: 1024, addSuffix: true, maxExponent: 3, minExponent: 3, suffix: 'i',
},
needParseSi: true
},
},
{
name: 'ip',
@@ -125,6 +125,7 @@ export default {
:resource="value"
:mode="mode"
:apply-hooks="applyHooks"
@error="e=>errors=e"
>
<Tabbed
v-if="spec"
@@ -171,6 +172,9 @@ export default {
:cpu="cpu"
:mode="mode"
:memory="memory"
:max-cpu="maxCpu"
:max-memory="maxMemory"
:enable-hot-plug="cpuMemoryHotplugEnabled"
/>
</div>
<div class="row">
@@ -118,6 +118,10 @@ export default {
imageName() {
return this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_NAME] || '-';
},
sourceType() {
return this.value?.spec?.sourceType;
},
}
};
</script>
@@ -252,6 +256,16 @@ export default {
</div>
</div>
<div class="row">
<div class="col span-12">
<LabelValue
:name="t('harvester.image.source')"
:value="sourceType"
class="mb-20"
/>
</div>
</div>
<div
v-if="errorMessage !== '-'"
class="row"
@@ -125,6 +125,7 @@ export default {
:resource="value"
:mode="mode"
:apply-hooks="applyHooks"
@error="e=>errors=e"
>
<Tabbed
v-if="spec"
@@ -171,6 +172,9 @@ export default {
:cpu="cpu"
:mode="mode"
:memory="memory"
:max-cpu="maxCpu"
:max-memory="maxMemory"
:enable-hot-plug="cpuMemoryHotplugEnabled"
/>
<div class="row mb-10">
@@ -5,6 +5,7 @@ import CreateEditView from '@shell/mixins/create-edit-view';
import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress';
import VMConsoleBar from '../../../components/VMConsoleBar';
import { HCI } from '../../../types';
import { getVmCPUMemoryValues } from '../../../utils/cpuMemory';
const UNDEFINED = 'n/a';
@@ -91,9 +92,9 @@ export default {
},
flavor() {
const domain = this.value?.spec?.template?.spec?.domain;
const { cpu, memory } = getVmCPUMemoryValues(this.value);
return `${ domain.cpu?.cores } vCPU , ${ domain.resources?.limits?.memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
return `${ cpu } vCPU , ${ memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
},
kernelRelease() {
@@ -17,6 +17,13 @@ export default {
default: () => {
return {};
}
},
vmimResource: {
type: Object,
required: true,
default: () => {
return {};
}
}
},
@@ -25,6 +32,12 @@ export default {
},
computed: {
liveMigrationProgressEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('liveMigrationProgress');
},
migrationPhase() {
return this.vmimResource?.status?.phase || 'N/A';
},
migrationState() {
return this.localResource?.status?.migrationState;
},
@@ -58,6 +71,18 @@ export default {
<template>
<div>
<div
v-if="liveMigrationProgressEnabled"
class="row mb-20"
>
<div class="col span-6">
<LabelValue
:name="t('harvester.virtualMachine.detail.details.phase')"
:value="migrationPhase"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabelValue
@@ -27,6 +27,8 @@ import { formatSi } from '@shell/utils/units';
const VM_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-detail-1/vm-info-detail?orgId=1';
const VM_MIGRATION_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-migration-details-1/harvester-vm-migration-details?orgId=1';
export default {
name: 'VMIDetailsPage',
@@ -63,6 +65,7 @@ export default {
hasResourceQuotaSchema: false,
switchToCloud: false,
VM_METRICS_DETAIL_URL,
VM_MIGRATION_DETAIL_URL,
showVmMetrics: false,
};
},
@@ -78,6 +81,7 @@ export default {
events: this.$store.dispatch(`${ inStore }/findAll`, { type: EVENT }),
allSSHs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SSH }),
vmis: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMI }),
vmims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIM }),
restore: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.RESTORE }),
};
@@ -131,6 +135,39 @@ export default {
return this.$store.getters[`${ inStore }/all`](EVENT);
},
vmim() {
const inStore = this.$store.getters['currentProduct'].inStore;
const vmimList = this.$store.getters[`${ inStore }/all`](HCI.VMIM) || [];
const vmiName = this.vmi?.name || '';
// filter the corresponding vmims by vmim.spec.vmiName and find the latest one by creationTimestamp
const vmim = vmimList.filter((VMIM) => VMIM?.spec?.vmiName === vmiName).sort((a, b) => {
if (a?.metadata?.creationTimestamp > b?.metadata?.creationTimestamp) {
return -1;
}
return 1;
});
return vmim.length > 0 && vmim[0] ? vmim[0] : null;
},
migrationEvents() {
const migrationVMName = this.vmim?.metadata.name || '';
if (migrationVMName === '') {
return [];
}
return this.allEvents.filter((e) => {
const { creationTimestamp } = this.value?.metadata || {};
const involvedName = e?.involvedObject?.name;
return involvedName === migrationVMName && e.firstTimestamp >= creationTimestamp;
}).sort((a, b) => a.lastTimestamp > b.lastTimestamp);
},
events() {
return this.allEvents.filter((e) => {
const { name, creationTimestamp } = this.value?.metadata || {};
@@ -138,7 +175,6 @@ export default {
const pvcName = this.value.persistentVolumeClaimName || [];
const involvedName = e?.involvedObject?.name;
const matchPVC = pvcName.find((name) => name === involvedName);
return (involvedName === name || involvedName === podName || matchPVC) && e.firstTimestamp >= creationTimestamp;
@@ -157,6 +193,10 @@ export default {
vm: this.value.name
};
},
liveMigrationProgressEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('liveMigrationProgress');
},
},
methods: {
@@ -346,6 +386,20 @@ export default {
<Migration
:value="value"
:vmi-resource="vmi"
:vmim-resource="vmim"
/>
<DashboardMetrics
v-if="showVmMetrics && liveMigrationProgressEnabled"
:detail-url="VM_MIGRATION_DETAIL_URL"
graph-height="640px"
:has-summary-and-detail="false"
:vars="graphVars"
class="mb-30"
/>
<Events
v-if="liveMigrationProgressEnabled"
:resource="vmi"
:events="migrationEvents"
/>
</Tab>
+20 -6
View File
@@ -1,12 +1,13 @@
<script>
import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex';
import { runStrategies as runStrategyOptions } from '../config/harvester-map';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import { Checkbox } from '@components/Form/Checkbox';
import AsyncButton from '@shell/components/AsyncButton';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default {
name: 'CloneVMModal',
@@ -14,7 +15,7 @@ export default {
emits: ['close'],
components: {
AsyncButton, Banner, Checkbox, Card, LabeledInput
AsyncButton, Banner, Checkbox, Card, LabeledInput, LabeledSelect
},
props: {
@@ -26,9 +27,11 @@ export default {
data() {
return {
name: '',
cloneData: true,
errors: []
name: '',
cloneData: true,
errors: [],
runStrategy: runStrategyOptions[1],
runStrategyOptions
};
},
@@ -38,6 +41,9 @@ export default {
actionResource() {
return this.resources[0];
},
vmCloneRunStrategyEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('vmCloneRunStrategy');
},
},
methods: {
@@ -58,7 +64,8 @@ export default {
// deep clone
try {
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
const payload = this.vmCloneRunStrategyEnabled ? { targetVm: this.name, runStrategy: this.runStrategy } : { targetVm: this.name };
const res = await this.actionResource.doAction('clone', payload, {}, false);
if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', {
@@ -106,6 +113,13 @@ export default {
:label="t('harvester.modal.cloneVM.name')"
required
/>
<LabeledSelect
v-if="vmCloneRunStrategyEnabled"
v-model:value="runStrategy"
label-key="harvester.virtualMachine.runStrategy"
:options="runStrategyOptions"
:mode="mode"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
-149
View File
@@ -1,149 +0,0 @@
<script>
import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import { Checkbox } from '@components/Form/Checkbox';
import AsyncButton from '@shell/components/AsyncButton';
import { LabeledInput } from '@components/Form/LabeledInput';
export default {
name: 'CloneVMModal',
emits: ['close'],
components: {
AsyncButton, Banner, Checkbox, Card, LabeledInput
},
props: {
resources: {
type: Array,
required: true
}
},
data() {
return {
name: '',
cloneData: true,
errors: []
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0];
},
},
methods: {
close() {
this.name = '';
this.$emit('close');
},
async create(buttonCb) {
// shadow clone
if (!this.cloneData) {
this.resources[0].goToClone();
buttonCb(false);
this.close();
return;
}
// deep clone
try {
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', {
title: this.t('harvester.notification.title.succeed'),
message: this.t('harvester.modal.cloneVM.message.success', { name: this.name })
}, { root: true });
this.close();
buttonCb(true);
} else {
const error = [res?.data] || exceptionToErrorsArray(res);
this['errors'] = error;
buttonCb(false);
}
} catch (err) {
const error = err?.data || err;
const message = exceptionToErrorsArray(error);
this['errors'] = message;
buttonCb(false);
}
}
},
};
</script>
<template>
<Card :show-highlight-border="false">
<template #title>
{{ t('harvester.modal.cloneVM.title') }}
</template>
<template #body>
<Checkbox
v-model:value="cloneData"
class="mb-10"
label-key="harvester.modal.cloneVM.type"
/>
<LabeledInput
v-show="cloneData"
v-model:value="name"
class="mb-20"
:label="t('harvester.modal.cloneVM.name')"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
:label="err"
color="error"
/>
</template>
<template #actions>
<div class="actions">
<div class="buttons">
<button
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="create"
:action-label="cloneData ? t('harvester.modal.cloneVM.action.create') : t('harvester.modal.cloneVM.action.clone')"
:disabled="cloneData && !name"
@click="create"
/>
</div>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -0,0 +1,185 @@
<script>
import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex';
import { getVmCPUMemoryValues } from '../utils/cpuMemory';
import UnitInput from '@shell/components/form/UnitInput';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import { GIBIBYTE } from '../utils/unit';
export default {
name: 'CPUMemoryHotplugModal',
emits: ['close'],
components: {
AsyncButton, Card, Banner, UnitInput
},
props: {
resources: {
type: Array,
required: true
}
},
data() {
const { cpu, memory } = getVmCPUMemoryValues(this.resources[0] || {});
return {
cpu,
memory,
errors: [],
GIBIBYTE
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0] || {};
},
maxResourcesMessage() {
const { maxCpu, maxMemory } = getVmCPUMemoryValues(this.actionResource);
if (maxCpu && maxMemory) {
return this.t('harvester.modal.cpuMemoryHotplug.maxResourcesMessage', { maxCpu, maxMemory });
}
return '';
}
},
methods: {
close() {
this.cpu = '';
this.memory = '';
this.$emit('close');
},
change() {
if (parseInt(this.memory, 10) < 1 ) {
this.memory = '1Gi';
}
if (this.cpu < 1) {
this.cpu = 1;
}
},
async save(buttonCb) {
if (this.actionResource) {
try {
const res = await this.actionResource.doAction('cpuAndMemoryHotplug', { sockets: this.cpu, memory: this.memory });
if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', {
title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.modal.cpuMemoryHotplug.success', { vm: this.actionResource.nameDisplay })
}, { root: true });
this.close();
buttonCb(true);
} else {
const error = [res?.data] || exceptionToErrorsArray(res);
this['errors'] = error;
buttonCb(false);
}
} catch (err) {
const error = err?.data || err;
const message = exceptionToErrorsArray(error);
this['errors'] = message;
buttonCb(false);
}
}
},
}
};
</script>
<template>
<Card
ref="modal"
name="modal"
:show-highlight-border="false"
>
<template #title>
<h4
v-clean-html="t('harvester.modal.cpuMemoryHotplug.title')"
class="text-default-text"
/>
</template>
<template #body>
<Banner
v-if="maxResourcesMessage"
:label="maxResourcesMessage"
color="info"
/>
<UnitInput
v-model:value="cpu"
:label="t('harvester.virtualMachine.input.cpu')"
:delay="0"
required
suffix="C"
class="mb-20"
:mode="mode"
:min="1"
@update:value="change"
/>
<UnitInput
v-model:value="memory"
:label="t('harvester.virtualMachine.input.memory')"
:mode="mode"
:input-exponent="3"
:delay="0"
:min="1"
:increment="1024"
:output-modifier="true"
:disabled="disabled"
:suffix="GIBIBYTE"
class="mb-20"
@update:value="change"
/>
<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"
@click="save"
/>
</div>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -10,6 +10,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { NAMESPACE, STORAGE_CLASS } from '@shell/config/types';
import { allHash } from '@shell/utils/promise';
import { isInternalStorageClass } from '../utils/storage-class';
export default {
name: 'HarvesterExportImageDialog',
@@ -105,11 +106,17 @@ export default {
}
const options = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
const isInternal = isInternalStorageClass(s.metadata?.name);
if (isInternal) {
label += ` (${ this.t('harvester.storage.internal.label') })`;
}
return {
label,
value: s.name,
value: s.name,
disabled: isInternal,
};
}) || [];
+248 -86
View File
@@ -1,4 +1,5 @@
<script>
import { NAMESPACE } from '@shell/config/types';
import { randomStr } from '@shell/utils/string';
import { exceptionToErrorsArray, stringify } from '@shell/utils/error';
import { LabeledInput } from '@components/Form/LabeledInput';
@@ -6,7 +7,15 @@ import AsyncButton from '@shell/components/AsyncButton';
import GraphCircle from '@shell/components/graph/Circle';
import { Banner } from '@components/Banner';
import AppModal from '@shell/components/AppModal';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import UnitInput from '@shell/components/form/UnitInput';
import { HCI } from '../types';
import { HCI_SETTING } from '../config/settings';
import { DOC } from '../config/doc-links';
import { docLink } from '../utils/feature-flags';
const SELECT_ALL = 'select_all';
const UNSELECT_ALL = 'unselect_all';
export default {
name: 'SupportBundle',
@@ -17,14 +26,36 @@ export default {
AsyncButton,
Banner,
AppModal,
LabeledSelect,
UnitInput
},
async fetch() {
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
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 = [];
}
},
data() {
return {
url: '',
description: '',
errors: [],
isOpen: false,
isOpen: false,
errors: [],
version: '',
clusterName: '',
url: '',
description: '',
namespaces: [],
defaultNamespaces: [],
timeout: '',
expiration: '',
nodeTimeout: '',
};
},
@@ -39,23 +70,51 @@ export default {
percentage() {
return this.$store.getters['harvester-common/getBundlePercentage'];
}
},
availableNamespaces() {
const allNamespaces = this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
const defaultNamespacesIds = this.defaultNamespaces.map((ns) => ns.id);
return allNamespaces.filter((ns) => !defaultNamespacesIds.includes(ns) || this.namespaces.includes(ns));
},
namespaceOptions() {
if (this.availableNamespaces.length === 0) return [];
const allSelected = this.namespaces.length === this.availableNamespaces.length &&
this.availableNamespaces.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, ...this.availableNamespaces];
},
docLink() {
const version = this.$store.getters['harvester-common/getServerVersion']();
return docLink(DOC.SUPPORT_BUNDLE_NAMESPACES, version);
},
customSupportBundleFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
},
},
watch: {
isShowBundleModal: {
immediate: true,
handler(show) {
if (show) {
this.$nextTick(() => {
this.isOpen = true;
});
} else {
this.isOpen = false;
this.url = '';
this.description = '';
}
},
immediate: true
this.isOpen = show;
}
},
isOpen(newVal) {
if (newVal) {
this.loadDefaultSettings();
} else {
this.resetForm();
}
},
},
@@ -65,33 +124,93 @@ export default {
close() {
this.isOpen = false;
this.$store.commit('harvester-common/toggleBundleModal', false);
this.backUpName = '';
},
loadDefaultSettings() {
const cluster = this.$store.getters['currentCluster'];
const versionSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SERVER_VERSION);
const namespacesSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES);
const timeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT);
const expirationSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION);
const nodeTimeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT);
this.version = versionSetting?.currentVersion || '';
this.clusterName = cluster?.id || '';
this.namespaces = (namespacesSetting?.value ?? namespacesSetting?.default ?? '').split(',').map((ns) => ns.trim()).filter((ns) => ns);
this.timeout = timeoutSetting?.value ?? timeoutSetting?.default ?? '';
this.expiration = expirationSetting?.value ?? expirationSetting?.default ?? '';
this.nodeTimeout = nodeTimeoutSetting?.value ?? nodeTimeoutSetting?.default ?? '';
this.url = '';
this.description = '';
this.errors = [];
},
resetForm() {
this.url = '';
this.description = '';
this.namespaces = [];
this.timeout = '';
this.expiration = '';
this.nodeTimeout = '';
this.errors = [];
},
updateNamespaces(selected) {
if (selected.includes(SELECT_ALL)) {
this.namespaces = [...this.availableNamespaces];
} else if (selected.includes(UNSELECT_ALL)) {
this.namespaces = [];
} else {
this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL);
}
},
updateNumberValue(field, value) {
if (value === '' || value === null || isNaN(value)) {
this[field] = '';
return;
}
const num = Number(value);
const isValid = Number.isInteger(num) && num >= 0;
this[field] = isValid ? String(num) : '';
},
onKeyDown(e) {
if (['e', 'E', '+', '-', '.'].includes(e.key)) {
e.preventDefault();
}
},
async save(buttonCb) {
this.errors = [];
const name = `bundle-${ randomStr(5).toLowerCase() }`;
const name = `bundle-${ this.clusterName }-${ this.version }-${ randomStr(5).toLowerCase() }`;
const namespace = 'harvester-system';
const spec = {
description: this.description.trim(),
...(this.url.trim() && { issueURL: this.url.trim() }),
...(this.namespaces.length > 0 && { extraCollectionNamespaces: this.namespaces }),
...(this.timeout !== '' && { timeout: Number(this.timeout) }),
...(this.expiration !== '' && { expiration: Number(this.expiration) }),
...(this.nodeTimeout !== '' && { nodeTimeout: Number(this.nodeTimeout) }),
};
const bundleCrd = {
apiVersion: 'harvesterhci.io/v1beta1',
type: HCI.SUPPORT_BUNDLE,
kind: 'SupportBundle',
metadata: {
name,
namespace
},
spec: {
issueURL: this.url,
description: this.description
}
metadata: { name, namespace },
spec,
};
const inStore = this.$store.getters['currentProduct'].inStore;
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
try {
const inStore = this.$store.getters['currentProduct'].inStore;
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
await bundleValue.save();
this.$store.commit('harvester-common/setLatestBundleId', `${ namespace }/${ name }`, { root: true });
@@ -118,69 +237,110 @@ export default {
@close="close"
>
<div class="p-20">
<h2>
{{ t('harvester.modal.bundle.title') }}
</h2>
<div
v-if="!bundlePending"
class="content"
>
<LabeledInput
v-model:value="url"
:label="t('harvester.modal.bundle.url')"
class="mb-20"
/>
<LabeledInput
v-model:value="description"
:label="t('harvester.modal.bundle.description')"
type="multiline"
:min-height="120"
required
/>
</div>
<div
v-else
class="content"
>
<div class="circle">
<h2>{{ t('harvester.modal.bundle.title') }}</h2>
<div class="content">
<div
v-if="bundlePending"
class="circle mb-20"
>
<GraphCircle
primary-stroke-color="green"
secondary-stroke-color="white"
secondary-stroke-color="lightgrey"
:stroke-width="6"
:percentage="percentage"
:show-text="true"
/>
</div>
</div>
<template v-else>
<p
v-if="customSupportBundleFeatureEnabled"
v-clean-html="t('harvester.modal.bundle.tip', { doc: docLink }, true)"
class="mb-20"
/>
<LabeledInput
v-model:value="url"
:label="t('harvester.modal.bundle.url')"
class="mb-10"
/>
<LabeledInput
v-model:value="description"
required
:label="t('harvester.modal.bundle.description')"
type="multiline"
:min-height="80"
class="mb-10"
/>
<div
v-for="(err, idx) in errors"
:key="idx"
>
<Banner
color="error"
:label="stringify(err)"
/>
</div>
<div class="footer mt-20">
<button
class="btn btn-sm role-secondary mr-10"
@click="close"
<template v-if="customSupportBundleFeatureEnabled">
<LabeledSelect
v-model:value="namespaces"
:label="t('harvester.modal.bundle.namespaces.label')"
:clearable="true"
:multiple="true"
:options="namespaceOptions"
class="mb-10 label-select"
:tooltip="t('harvester.modal.bundle.namespaces.tooltip', _, true)"
@update:value="updateNamespaces"
/>
<UnitInput
v-model:value="timeout"
:label="t('harvester.modal.bundle.timeout.label')"
class="mb-10"
type="number"
:min="0"
:tooltip="t('harvester.modal.bundle.timeout.tooltip', _, true)"
:suffix="timeout > 1 ? 'Minutes' : 'Minute'"
@keydown="onKeyDown"
@update:value="val => updateNumberValue('timeout', val)"
/>
<UnitInput
v-model:value="expiration"
:label="t('harvester.modal.bundle.expiration.label')"
class="mb-10"
type="number"
:min="0"
:tooltip="t('harvester.modal.bundle.expiration.tooltip', _, true)"
:suffix="expiration > 1 ? 'Minutes' : 'Minute'"
@keydown="onKeyDown"
@update:value="val => updateNumberValue('expiration', val)"
/>
<UnitInput
v-model:value="nodeTimeout"
:label="t('harvester.modal.bundle.nodeTimeout.label')"
class="mb-10"
type="number"
:min="0"
:tooltip="t('harvester.modal.bundle.nodeTimeout.tooltip', _, true)"
:suffix="nodeTimeout > 1 ? 'Minutes' : 'Minute'"
@keydown="onKeyDown"
@update:value="val => updateNumberValue('nodeTimeout', val)"
/>
</template>
</template>
<div
v-for="(err, idx) in errors"
:key="idx"
>
{{ t('generic.close') }}
</button>
<AsyncButton
type="submit"
mode="generate"
class="btn btn-sm bg-primary"
:disabled="bundlePending"
@click="save"
/>
<Banner
color="error"
:label="stringify(err)"
/>
</div>
<div class="footer mt-20">
<button
class="btn btn-sm role-secondary mr-10"
@click="close"
>
{{ t('generic.close') }}
</button>
<AsyncButton
type="submit"
mode="generate"
class="btn btn-sm bg-primary"
:disabled="bundlePending"
@click="save"
/>
</div>
</div>
</div>
</app-modal>
@@ -194,6 +354,10 @@ export default {
max-height: 100vh;
}
.labeled-select.taggable ::v-deep(.vs__selected-options .vs__selected.vs__selected > button) {
margin: 0 7px;
}
.bundle {
cursor: pointer;
color: var(--primary);
@@ -204,10 +368,8 @@ export default {
}
.content {
height: 218px;
.circle {
padding-top: 20px;
padding: 10px 0;
height: 160px;
}
}
@@ -73,8 +73,10 @@ export default {
data.spec.clusterNetwork = this.clusterNetwork;
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl'](`v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`);
await this.$store.dispatch('harvester/request', {
url: `/v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`,
url,
method: 'PUT',
data,
});
@@ -38,6 +38,7 @@ export default {
:mode="mode"
:errors="errors"
@finish="save"
@error="e=>errors=e"
>
<component
:is="currentComponent"
@@ -80,6 +80,7 @@ export default {
:apply-hooks="applyHooks"
@finish="save"
@cancel="done"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
@@ -75,6 +75,7 @@ export default {
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<div class="header mb-20">
<FileSelector
@@ -191,7 +191,7 @@ export default {
:validation-passed="true"
:errors="errors"
:can-yaml="true"
@error="e=>errors = e"
@error="e=>errors=e"
@finish="save"
@cancel="done"
>
@@ -189,7 +189,7 @@ export default {
:resource="value"
:subtypes="[]"
:cancel-event="true"
@error="e=>errors = e"
@error="e=>errors=e"
@finish="save"
@cancel="done"
>
@@ -6,14 +6,13 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import { RadioGroup } from '@components/Form/Radio';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../types';
import { NETWORK_TYPE } from '../config/types';
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
const { L2VLAN, UNTAGGED, OVERLAY } = NETWORK_TYPE;
const AUTO = 'auto';
const MANUAL = 'manual';
@@ -44,13 +43,10 @@ export default {
data() {
const config = JSON.parse(this.value.spec.config);
const annotations = this.value?.metadata?.annotations || {};
const layer3Network = JSON.parse(annotations[HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE] || '{}');
if ((config.bridge || '').endsWith('-br')) {
config.bridge = config.bridge.slice(0, -3);
}
const type = this.value.vlanType || L2VLAN ;
return {
@@ -78,6 +74,30 @@ export default {
},
computed: {
clusterBridge: {
get() {
if (!this.config.bridge) {
return '';
}
// remove -br suffix if exists
return this.config?.bridge?.endsWith('-br') ? this.config.bridge.slice(0, -3) : '';
},
set(neu) {
if (neu === '') {
this.config.bridge = '';
return;
}
if (!neu.endsWith('-br')) {
this.config.bridge = `${ neu }-br`;
} else {
this.config.bridge = neu;
}
}
},
modeOptions() {
return [{
label: this.t('harvester.network.layer3Network.mode.auto'),
@@ -88,6 +108,14 @@ export default {
}];
},
kubeovnVpcSubnetSupport() {
return this.$store.getters['harvester-common/getFeatureEnabled']('kubeovnVpcSubnet');
},
longhornV2LVMSupport() {
return this.$store.getters['harvester-common/getFeatureEnabled']('longhornV2LVMSupport');
},
clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
@@ -103,8 +131,30 @@ export default {
});
},
networkType() {
return [L2VLAN, UNTAGGED];
networkTypes() {
const types = [L2VLAN, UNTAGGED];
if (this.kubeovnVpcSubnetSupport) {
types.push(OVERLAY);
}
return types;
},
isL2VlanNetwork() {
if (this.isView) {
return this.value.vlanType === L2VLAN;
}
return this.type === L2VLAN;
},
isOverlayNetwork() {
if (this.isView) {
return this.value.vlanType === OVERLAY;
}
return this.type === OVERLAY;
},
isUntaggedNetwork() {
@@ -116,36 +166,54 @@ export default {
}
},
watch: {
type(newType) {
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 {
this.config.type = 'bridge';
this.config.promiscMode = true;
this.config.ipam = {};
this.config.bridge = '';
delete this.config.provider;
delete this.config.server_socket;
}
}
},
methods: {
async saveNetwork(buttonCb) {
const errors = [];
if (!this.config.vlan && !this.isUntaggedNetwork) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
}
if (!this.config.bridge) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') }));
}
if (this.layer3Network.mode === MANUAL) {
if (!this.layer3Network.gateway) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') }));
if (this.isL2VlanNetwork || this.isUntaggedNetwork) {
if (!this.config.vlan && !this.isUntaggedNetwork) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
}
if (!this.layer3Network.cidr) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') }));
if (!this.config.bridge) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') }));
}
if (this.layer3Network.mode === MANUAL) {
if (!this.layer3Network.gateway) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') }));
}
if (!this.layer3Network.cidr) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') }));
}
}
if (errors.length > 0) {
buttonCb(false);
this.errors = errors;
return false;
}
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
}
if (errors.length > 0) {
buttonCb(false);
this.errors = errors;
return false;
}
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
await this.save(buttonCb);
},
@@ -169,14 +237,19 @@ export default {
updateBeforeSave() {
this.config.name = this.value.metadata.name;
if (this.isOverlayNetwork) {
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
delete this.config.bridge;
delete this.config.promiscMode;
delete this.config.vlan;
delete this.config.ipam;
}
if (this.isUntaggedNetwork) {
delete this.config.vlan;
}
this.value.spec.config = JSON.stringify({
...this.config,
bridge: `${ this.config.bridge }-br`,
});
this.value.spec.config = JSON.stringify({ ...this.config });
},
}
};
@@ -190,6 +263,7 @@ export default {
:errors="errors"
:apply-hooks="applyHooks"
@finish="saveNetwork"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"
@@ -211,14 +285,15 @@ export default {
<LabeledSelect
v-model:value="type"
class="mb-20"
:options="networkType"
:options="networkTypes"
:mode="mode"
:disabled="isEdit"
:label="t('harvester.fields.type')"
required
/>
<LabeledInput
v-if="!isUntaggedNetwork"
v-if="isL2VlanNetwork"
v-model:value.number="config.vlan"
class="mb-20"
required
@@ -228,25 +303,20 @@ export default {
:mode="mode"
@update:value="input"
/>
<div class="row">
<div
class="col span-12"
>
<LabeledSelect
v-model:value="config.bridge"
class="mb-20"
:label="t('harvester.network.clusterNetwork.label')"
required
:options="clusterNetworkOptions"
:mode="mode"
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
/>
</div>
</div>
<LabeledSelect
v-if="!isOverlayNetwork"
v-model:value="clusterBridge"
class="mb-20"
:label="t('harvester.network.clusterNetwork.label')"
required
:disabled="isEdit"
:options="clusterNetworkOptions"
:mode="mode"
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
/>
</Tab>
<Tab
v-if="!isUntaggedNetwork"
v-if="isL2VlanNetwork"
name="layer3Network"
:label="t('harvester.network.tabs.layer3Network')"
:weight="98"
@@ -198,7 +198,7 @@ export default {
:apply-hooks="applyHooks"
:validation-passed="canSave"
@finish="save"
@error="e=>errors = e"
@error="e=>errors=e"
>
<div class="banner">
<Banner
@@ -84,6 +84,10 @@ export default {
this.value['_type'] = TYPES.OPAQUE;
}
if ( this.realMode === _CLONE) {
this.value['_type'] = this.liveValue._type;
}
return {
isCloud,
nodeDrivers: null,
@@ -234,7 +238,7 @@ export default {
:resource="value"
:errors="errors"
@finish="saveSecret"
@error="e=>errors = e"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
+39 -4
View File
@@ -4,8 +4,10 @@ import { RadioGroup } from '@components/Form/Radio';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { TextAreaAutoGrow } from '@components/Form/TextArea';
import UnitInput from '@shell/components/form/UnitInput';
import CreateEditView from '@shell/mixins/create-edit-view';
import { DOC } from '../config/doc-links';
import { docLink } from '../utils/feature-flags';
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
@@ -15,7 +17,8 @@ export default {
LabeledInput,
LabeledSelect,
RadioGroup,
TextAreaAutoGrow
TextAreaAutoGrow,
UnitInput
},
mixins: [CreateEditView],
@@ -57,7 +60,13 @@ export default {
return {
setting,
description: isHarvester ? t(`advancedSettings.descriptions.harv-${ this.value.id }`) : t(`advancedSettings.descriptions.${ this.value.id }`),
description: isHarvester ? t(
`advancedSettings.descriptions.harv-${ this.value.id }`,
this.getDocLinkParams()
) : t(
`advancedSettings.descriptions.${ this.value.id }`,
this.getDocLinkParams()
),
editHelp: t(`advancedSettings.editHelp.${ this.value.id }`),
enumOptions,
canReset,
@@ -128,6 +137,13 @@ export default {
await this.clusterRegistrationUrlTip();
}
// remove any leading zeros (e.g. '0123' becomes 123)
if (this.setting.kind === 'number' && this.value.value) {
const num = Number(this.value.value);
this.value.value = isNaN(num) ? 0 : `${ num }`;
}
this.save(done);
},
@@ -163,6 +179,18 @@ export default {
if (typeof this.$refs.settingComp?.useDefault === 'function') {
this.$refs.settingComp.useDefault();
}
},
getDocLinkParams() {
const setting = HCI_ALLOWED_SETTINGS[this.value.id] || HCI_SINGLE_CLUSTER_ALLOWED_SETTING[this.value.id];
if (setting?.docPath) {
const version = this.$store.getters['harvester-common/getServerVersion']();
const url = docLink(DOC[setting.docPath], version);
return { url };
}
return {};
}
}
};
@@ -177,7 +205,7 @@ export default {
:subtypes="[]"
:can-yaml="false"
:cancel-event="true"
@error="e => (errors = e)"
@error="e=>errors=e"
@finish="saveSettings"
@cancel="done"
>
@@ -238,6 +266,13 @@ export default {
:min-height="254"
/>
</div>
<div v-else-if="setting.kind === 'number'">
<LabeledInput
v-model:value="value.value"
:label="t('advancedSettings.edit.value')"
type="number"
/>
</div>
<div v-else>
<LabeledInput
v-model:value="value.value"
@@ -0,0 +1,319 @@
<script>
import { VOLUME_MODE } from '@pkg/harvester/config/types';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import ArrayList from '@shell/components/form/ArrayList';
import { Checkbox } from '@components/Form/Checkbox';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { VOLUME_SNAPSHOT_CLASS, HCI } from '../../types';
import { HCI_SETTING } from '../../config/settings';
import { allHash } from '@shell/utils/promise';
import { _EDIT, _CREATE } from '@shell/config/query-params';
export default {
name: 'CDISettings',
props: {
value: {
type: Object,
required: true
},
mode: {
type: String,
required: true
},
provisioner: {
type: String,
required: true
},
},
components: {
ArrayList,
Checkbox,
LabeledInput,
LabeledSelect,
},
emits: ['update:cdiSettings'],
async fetch() {
const hash = {
volumeSnapshotClasses: this.$store.dispatch(`${ this.inStore }/findAll`, { type: VOLUME_SNAPSHOT_CLASS }),
csiDriverConfigSetting: this.$store.dispatch(`${ this.inStore }/find`, { type: HCI.SETTING, id: HCI_SETTING.CSI_DRIVER_CONFIG }),
};
await allHash(hash);
if (this.mode === _CREATE ) {
this.setDefaultVolumeSnapshotClass();
} else {
this.initCDISettingsFromAnnotations();
}
},
data() {
return {
cdiSettings: {
volumeModeAccessModes: [],
volumeSnapshotClass: null,
cloneStrategy: null,
filesystemOverhead: null,
},
defaultAddValue: { volumeMode: null, accessModes: [] },
noneOption: { label: 'None', value: '' },
};
},
computed: {
inStore() {
return this.$store.getters['currentProduct'].inStore;
},
allVolumeModeOptions() {
return Object.values(VOLUME_MODE).map((mode) => ({ label: mode, value: mode }));
},
selectedVolumeModes() {
return this.cdiSettings.volumeModeAccessModes
.map((item) => item.volumeMode)
.filter(Boolean);
},
volumeModeOptions() {
return [this.noneOption, ...this.allVolumeModeOptions].filter( (option) => !this.selectedVolumeModes.includes(option.value) );
},
allVolumeModesSelected() {
return this.selectedVolumeModes.length === this.allVolumeModeOptions.length;
},
accessModeOptions() {
return [
{ label: 'ReadWriteOnce', value: 'ReadWriteOnce' },
{ label: 'ReadOnlyMany', value: 'ReadOnlyMany' },
{ label: 'ReadWriteMany', value: 'ReadWriteMany' },
{ label: 'ReadWriteOncePod', value: 'ReadWriteOncePod' },
];
},
cloneStrategyOptions() {
return [
this.noneOption,
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.copy'), value: 'copy' },
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.snapshot'), value: 'snapshot' },
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.csiClone'), value: 'csi-clone' },
];
},
volumeSnapshotClassOptions() {
const allClasses = this.$store.getters[`${ this.inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
const filtered = allClasses.filter((c) => c.driver === this.provisioner);
return [this.noneOption, ...filtered.map((c) => ({ label: c.name, value: c.name }))];
},
isFilesystemOverheadValid() {
const val = this.cdiSettings.filesystemOverhead;
if (val === null || val === '') return true;
const regex = /^(0(\.\d{1,3})?|1(\.0{1,3})?)$/;
return regex.test(val);
},
isCustomClass() {
return this.mode === _CREATE || this.mode === _EDIT;
}
},
watch: {
provisioner() {
this.resetCdiSettings();
this.$nextTick(this.setDefaultVolumeSnapshotClass);
},
cdiSettings: {
handler(val) {
this.$emit('update:cdiSettings', val);
},
deep: true,
immediate: true,
}
},
methods: {
setDefaultVolumeSnapshotClass() {
try {
const setting = this.$store.getters[`${ this.inStore }/byId`](HCI.SETTING, HCI_SETTING.CSI_DRIVER_CONFIG);
const config = JSON.parse(setting?.value || setting?.default || '{}');
const defaultClass = config?.[this.provisioner]?.volumeSnapshotClassName || null;
const allClasses = this.$store.getters[`${ this.inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
const matched = allClasses.find((cls) => cls.name === defaultClass && cls.driver === this.provisioner);
this.cdiSettings.volumeSnapshotClass = matched?.name || null;
} catch (e) {
console.error('Failed to parse CSI config:', e); // eslint-disable-line no-console
this.cdiSettings.volumeSnapshotClass = null;
}
},
initCDISettingsFromAnnotations() {
const annotations = this.value?.metadata?.annotations || {};
let volumeModeAccessModes = [];
const rawVolumeMode = annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES];
if (rawVolumeMode) {
try {
const parsed = JSON.parse(rawVolumeMode);
volumeModeAccessModes = Object.entries(parsed).map(([volumeMode, accessModes]) => ({
volumeMode,
accessModes: Array.isArray(accessModes) ? accessModes : [],
}));
} catch (e) {
console.error('Failed to parse annotation:', e); // eslint-disable-line no-console
}
}
if (volumeModeAccessModes.length) {
this.cdiSettings.volumeModeAccessModes = volumeModeAccessModes;
}
if (annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS]) {
this.cdiSettings.volumeSnapshotClass = annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS];
}
if (annotations[HCI_ANNOTATIONS.CLONE_STRATEGY]) {
this.cdiSettings.cloneStrategy = annotations[HCI_ANNOTATIONS.CLONE_STRATEGY];
}
if (annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD]) {
this.cdiSettings.filesystemOverhead = annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD];
}
},
resetCdiSettings() {
this.cdiSettings.volumeModeAccessModes = [this.defaultAddValue];
this.cdiSettings.volumeSnapshotClass = null;
this.cdiSettings.cloneStrategy = null;
this.cdiSettings.filesystemOverhead = null;
},
validateFilesystemOverhead(e) {
const val = e.target.value;
this.cdiSettings.filesystemOverhead = val;
}
}
};
</script>
<template>
<ArrayList
v-model:value="cdiSettings.volumeModeAccessModes"
:initial-empty-row="true"
:show-header="true"
:mode="mode"
:title="t('harvester.storage.cdiSettings.volumeModeAccessModes.label')"
:add-label="t('harvester.storage.cdiSettings.volumeModeAccessModes.add')"
:default-add-value="defaultAddValue"
:protip="t('harvester.storage.cdiSettings.volumeModeAccessModes.tooltip')"
:add-disabled="allVolumeModesSelected"
>
<template #column-headers>
<div class="column-headers">
<div
class="row"
:class="{ custom: isCustomClass }"
>
<label
class="col span-3 value text-label mb-10"
for="volumeMode"
>
{{ t('harvester.storage.cdiSettings.volumeModeAccessModes.volumeMode') }}
</label>
<label
class="col span-9 value text-label mb-10"
for="accessModes"
>
{{ t('harvester.storage.cdiSettings.volumeModeAccessModes.accessModes.label') }}
</label>
</div>
</div>
</template>
<template #columns="{ row }">
<div class="row">
<div class="col span-3">
<LabeledSelect
id="volumeMode"
v-model:value="row.value.volumeMode"
:mode="mode"
:options="volumeModeOptions"
/>
</div>
<div
id="accessModes"
class="col span-9"
>
<Checkbox
v-for="opt in accessModeOptions"
:key="opt.value"
v-model:value="row.value.accessModes"
:value-when-true="opt.value"
:label="opt.label"
type="checkbox"
:mode="mode"
/>
</div>
</div>
</template>
</ArrayList>
<LabeledSelect
v-model:value="cdiSettings.volumeSnapshotClass"
class="select mt-20 mb-20"
:label="t('harvester.storage.cdiSettings.volumeSnapshotClass.label')"
:tooltip="t('harvester.storage.cdiSettings.volumeSnapshotClass.tooltip')"
:mode="mode"
:options="volumeSnapshotClassOptions"
/>
<LabeledSelect
v-model:value="cdiSettings.cloneStrategy"
class="select mb-20"
:label="t('harvester.storage.cdiSettings.cloneStrategy.label')"
:tooltip="t('harvester.storage.cdiSettings.cloneStrategy.tooltip')"
:mode="mode"
:options="cloneStrategyOptions"
/>
<LabeledInput
v-model:value="cdiSettings.filesystemOverhead"
class="select mb-20"
:label="t('harvester.storage.cdiSettings.fileSystemOverhead.label')"
:tooltip="t('harvester.storage.cdiSettings.fileSystemOverhead.tooltip')"
:mode="mode"
type="number"
:min="0"
:max="1"
:step="0.001"
:placeholder="t('harvester.storage.cdiSettings.fileSystemOverhead.placeholder')"
:status="isFilesystemOverheadValid ? null : 'error'"
@input="validateFilesystemOverhead"
/>
</template>
<style scoped lang="scss">
.column-headers .row.custom {
max-width: calc(100% - 75px);
}
.row {
align-items: center;
}
.select {
max-width: 480px;
}
</style>
@@ -1,4 +1,5 @@
<script>
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import CreateEditView from '@shell/mixins/create-edit-view';
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
@@ -6,11 +7,9 @@ import ArrayList from '@shell/components/form/ArrayList';
import Tab from '@shell/components/Tabbed/Tab';
import Tabbed from '@shell/components/Tabbed';
import { RadioGroup } from '@components/Form/Radio';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import Loading from '@shell/components/Loading';
import { _CREATE, _VIEW } from '@shell/config/query-params';
import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
import {
@@ -22,6 +21,7 @@ import { CSI_DRIVER } from '../../types';
import Tags from '../../components/DiskTags';
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
import CDISettings from './CDISettings';
export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node';
@@ -45,6 +45,7 @@ export default {
Tabbed,
Loading,
Tags,
CDISettings,
},
mixins: [CreateEditView],
@@ -117,6 +118,7 @@ export default {
key: '',
values: [],
},
cdiSettings: {},
};
},
@@ -124,10 +126,10 @@ export default {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = {
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER })
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }),
};
if (this.value.longhornV2LVMSupport) {
@@ -197,6 +199,14 @@ export default {
return v2DataEngine.value === 'true' ? DATA_ENGINE_V2 : DATA_ENGINE_V1;
},
isCDISettingsFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('cdiSettings');
},
shouldShowCDISettingsTab() {
return this.isCDISettingsFeatureEnabled && this.provisioner !== `${ LONGHORN_DRIVER }_${ DATA_ENGINE_V1 }`;
},
},
watch: {
@@ -249,6 +259,10 @@ export default {
});
this.formatAllowedTopoloties();
if (this.shouldShowCDISettingsTab) {
this.formatCDISettings();
}
},
formatAllowedTopoloties() {
@@ -270,6 +284,42 @@ export default {
if (matchLabelExpressions.length > 0) {
this.value.allowedTopologies = [{ matchLabelExpressions: [...matchLabelExpressions, ...lvmMatchExpression] }];
}
},
formatCDISettings() {
const annotations = this.value.metadata.annotations || {};
const volumeModeAccessModes = {};
this.cdiSettings.volumeModeAccessModes.forEach((setting) => {
if (setting.volumeMode && Array.isArray(setting.accessModes) && setting.accessModes.length > 0) {
volumeModeAccessModes[setting.volumeMode] = setting.accessModes;
}
});
if (Object.keys(volumeModeAccessModes).length > 0) {
annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES] = JSON.stringify(volumeModeAccessModes);
} else {
delete annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES];
}
if (this.cdiSettings.volumeSnapshotClass) {
annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS] = this.cdiSettings.volumeSnapshotClass;
} else {
delete annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS];
}
if (this.cdiSettings.cloneStrategy) {
annotations[HCI_ANNOTATIONS.CLONE_STRATEGY] = this.cdiSettings.cloneStrategy;
} else {
delete annotations[HCI_ANNOTATIONS.CLONE_STRATEGY];
}
if (this.cdiSettings.filesystemOverhead) {
annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD] = this.cdiSettings.filesystemOverhead;
} else {
delete annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD];
}
this.value.metadata.annotations = annotations;
}
}
};
@@ -286,7 +336,7 @@ export default {
:validation-passed="true"
:apply-hooks="applyHooks"
:errors="errors"
@error="e=>errors = e"
@error="e=>errors=e"
@finish="save"
@cancel="done"
>
@@ -404,6 +454,19 @@ export default {
</template>
</ArrayList>
</Tab>
<Tab
v-if="shouldShowCDISettingsTab"
name="cdiSettings"
:label="t('harvester.storage.cdiSettings.label')"
:weight="-2"
>
<CDISettings
v-model:cdi-settings="cdiSettings"
:value="value"
:mode="mode"
:provisioner="value.provisioner"
/>
</Tab>
</Tabbed>
</CruResource>
</template>
@@ -17,6 +17,7 @@ import { VM_IMAGE_FILE_FORMAT } from '../validators/vm-image';
import { OS } from '../mixins/harvester-vm';
import { HCI } from '../types';
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
import { isInternalStorageClass } from '../utils/storage-class';
const ENCRYPT = 'encrypt';
const DECRYPT = 'decrypt';
@@ -110,6 +111,7 @@ export default {
headers: {},
fileUrl: '',
file: '',
HCI_ANNOTATIONS,
};
},
@@ -167,11 +169,18 @@ export default {
return filteredStorages
.map((s) => {
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
let disabled = false;
if (isInternalStorageClass(s.name)) {
label += ` (${ this.t('harvester.storage.internal.label') })`;
disabled = true;
}
return {
label,
value: s.name,
disabled
};
}) || [];
},
@@ -595,6 +604,8 @@ export default {
:pad-left="false"
:read-allowed="false"
:value-can-be-empty="true"
:toggle-filter="true"
:protected-keys="[HCI_ANNOTATIONS.IMAGE_DISPLAY_NAME]"
@focusKey="focusKey"
@update:value="value.setLabels($event)"
>
@@ -611,7 +622,7 @@ export default {
<input
v-else
v-model="row[valueName]"
:disabled="isView"
:disabled="isView || row[keyName] === HCI_ANNOTATIONS.IMAGE_DISPLAY_NAME"
:type="'text'"
:placeholder="t('keyValue.valuePlaceholder')"
autocorrect="off"
@@ -189,6 +189,16 @@ export default {
}
this.value.spec['templateId'] = `${ namespace }/${ name }`;
// inherit labels and annotations so the VM gets them when created from the template
this.value.spec.vm.metadata.labels = {
...this.value.spec.vm.metadata.labels,
...this.value.metadata.labels
};
this.value.spec.vm.metadata.annotations = {
...this.value.spec.vm.metadata.annotations,
...this.value.metadata.annotations
};
const res = await this.value.save();
await this.saveSecret(res);
@@ -219,6 +229,7 @@ export default {
:errors="errors"
:apply-hooks="applyHooks"
@finish="saveVMT"
@error="e=>errors=e"
>
<NameNsDescription
:value="templateValue"
@@ -246,8 +257,12 @@ export default {
:label="t('harvester.vmTemplate.tabs.basics')"
>
<CpuMemory
class="mb-20"
:cpu="cpu"
:memory="memory"
:max-cpu="maxCpu"
:max-memory="maxMemory"
:enable-hot-plug="cpuMemoryHotplugEnabled"
:disabled="isConfig"
@updateCpuMemory="updateCpuMemory"
/>
@@ -358,6 +373,26 @@ export default {
@update:value="value.setInstanceLabels($event)"
/>
</Tab>
<Tab
name="annotations"
:label="t('harvester.tab.annotations')"
:weight="-11"
>
<Banner color="info">
<t k="harvester.virtualMachine.annotations.banner" />
</Banner>
<KeyValue
key="annotations"
:value="value.annotations"
:protected-keys="value.systemAnnotations || []"
:toggle-filter="true"
:add-label="t('labels.addAnnotation')"
:mode="mode"
:read-allowed="false"
:value-can-be-empty="true"
@update:value="value.setAnnotations($event)"
/>
</Tab>
<Tab
name="advanced"
:label="t('harvester.tab.advanced')"
+15 -1
View File
@@ -23,6 +23,7 @@ import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
import { DATA_ENGINE_V2 } from '../models/harvester/persistentvolumeclaim';
import { GIBIBYTE } from '../utils/unit';
import { VOLUME_MODE } from '@pkg/harvester/config/types';
import { isInternalStorageClass } from '../utils/storage-class';
export default {
name: 'HarvesterVolume',
@@ -206,11 +207,18 @@ export default {
storageClassOptions() {
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
let disabled = false;
if (isInternalStorageClass(s.name)) {
label += ` (${ this.t('harvester.storage.internal.label') })`;
disabled = true;
}
return {
label,
value: s.name,
disabled
};
}) || [];
},
@@ -304,6 +312,10 @@ export default {
return ['ReadWriteMany'];
}
if (this.value?.spec?.accessModes && this.value?.spec?.accessModes?.length > 0) {
return this.value.spec.accessModes;
}
const storageClassName = this.value.spec.storageClassName;
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
@@ -374,10 +386,12 @@ export default {
:generate-yaml="generateYaml"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
:namespaced="true"
:name-required="false"
:mode="mode"
@update:value="$emit('update:value', $event)"
/>
@@ -64,6 +64,7 @@ export default {
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
@@ -0,0 +1,323 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
import Tab from '@shell/components/Tabbed/Tab';
import { NETWORK_ATTACHMENT } from '@shell/config/types';
import Loading from '@shell/components/Loading';
import CreateEditView from '@shell/mixins/create-edit-view';
import { RadioGroup } from '@components/Form/Radio';
import { NETWORK_PROTOCOL, NETWORK_TYPE } from '@pkg/harvester/config/types';
import { set } from '@shell/utils/object';
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';
export default {
name: 'EditSubnet',
emits: ['update:value'],
components: {
CruResource,
LabeledInput,
LabeledSelect,
NameNsDescription,
Tab,
RadioGroup,
ArrayList,
ResourceTabs,
Loading,
},
mixins: [CreateEditView],
inheritAttrs: false,
created() {
const vpc = this.$route.query.vpc || '';
set(this.value, 'spec', this.value.spec || {
cidrBlock: '',
protocol: NETWORK_PROTOCOL.IPv4,
provider: '',
vpc,
gatewayIP: '',
excludeIps: [],
private: false
});
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = {
vpc: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
nad: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }),
};
await allHash(hash);
},
computed: {
showAllowSubnets() {
return this.value?.spec?.private === true;
},
doneLocationOverride() {
return this.value.doneOverride;
},
allowSubnetTooltip() {
return this.t('harvester.subnet.allowSubnet.tooltip', null, true);
},
excludeIPsTooltip() {
return this.t('harvester.subnet.excludeIPs.tooltip', null, true);
},
protocolOptions() {
return Object.values(NETWORK_PROTOCOL);
},
provider: {
get() {
const raw = this.value.spec.provider;
if (!raw) {
return '';
}
const vmNet = raw.split('.')[0] || '';
const ns = raw.split('.')[1] || '';
return `${ ns }/${ vmNet }`;
},
set(value) {
const ns = value.split('/')[0] || '';
const vmNet = value.split('/')[1] || '';
const provider = `${ vmNet }.${ ns }.ovn`;
set(this.value, 'spec.provider', provider);
}
},
providerOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const vmNets = this.$store.getters[`${ inStore }/all`](NETWORK_ATTACHMENT) || [];
return vmNets.filter((net) => net.vlanType === NETWORK_TYPE.OVERLAY).map((n) => ({
label: n.id,
value: n.id,
}));
},
vpcOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const vpcs = this.$store.getters[`${ inStore }/all`](HCI.VPC) || [];
return vpcs.map((n) => ({
label: n.id,
value: n.id,
}));
}
},
methods: {
async saveSubnet(buttonCb) {
const errors = [];
const name = this.value?.metadata?.name;
try {
if (!name) {
errors.push(this.t('validation.required', { key: this.t('generic.name') }, true));
} else if (!this.value?.spec?.cidrBlock) {
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.cidrBlock.label') }, true));
} else if (!this.value?.spec?.provider) {
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'));
}
if (errors.length > 0) {
buttonCb(false);
this.errors = errors;
return false;
}
await this.value.save();
buttonCb(true);
this.done();
} catch (e) {
this.errors = [e];
buttonCb(false);
}
},
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<CruResource
v-else
:done-route="doneRoute"
:resource="value"
:mode="mode"
:apply-hooks="applyHooks"
:errors="errors"
@finish="saveSubnet"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-events="false"
:need-related="false"
:mode="mode"
:side-tabs="true"
>
<Tab
name="Basic"
:label="t('generic.basic')"
:weight="-1"
class="bordered-table"
>
<div class="row mt-10">
<div class="col span-6">
<LabeledInput
v-model:value="value.spec.cidrBlock"
class="mb-20"
required
:placeholder="t('harvester.subnet.cidrBlock.placeholder')"
:label="t('harvester.subnet.cidrBlock.label')"
:mode="mode"
/>
</div>
<div class="col span-6">
<LabeledSelect
v-model:value="value.spec.protocol"
:label="t('harvester.subnet.protocol.label')"
:options="protocolOptions"
required
:mode="mode"
/>
</div>
</div>
<div class="row mt-10">
<div class="col span-6">
<LabeledSelect
v-model:value="provider"
:label="t('harvester.subnet.provider.label')"
:options="providerOptions"
:tooltip="t('harvester.subnet.provider.tooltip')"
required
:mode="mode"
/>
</div>
<div class="col span-6">
<LabeledSelect
v-model:value="value.spec.vpc"
:label="t('harvester.subnet.vpc.label')"
:options="vpcOptions"
required
:disabled="true"
:mode="mode"
/>
</div>
</div>
<div class="row mt-20">
<div class="col span-6">
<LabeledInput
v-model:value="value.spec.gateway"
class="mb-20"
:placeholder="t('harvester.subnet.gateway.placeholder')"
:label="t('harvester.subnet.gateway.label')"
:mode="mode"
/>
</div>
</div>
<div class="row mt-20">
<div class="col span-6">
<RadioGroup
v-model:value="value.spec.private"
name="enabled"
:options="[true, false]"
:label="t('harvester.subnet.private.label')"
:labels="[t('generic.enabled'), t('generic.disabled')]"
:mode="mode"
tooltip-key="harvester.subnet.private.tooltip"
/>
</div>
</div>
<ArrayList
v-if="showAllowSubnets"
v-model:value="value.spec.allowSubnets"
:show-header="true"
class="mt-20"
:mode="mode"
:add-label="t('harvester.subnet.allowSubnet.addSubnet')"
>
<template #column-headers>
<div class="box">
<h3 class="key">
{{ t('harvester.subnet.allowSubnet.label') }}
<i
v-clean-tooltip="{content: allowSubnetTooltip, triggers: ['hover', 'touch', 'focus'] }"
v-stripped-aria-label="allowSubnetTooltip"
class="icon icon-info"
tabindex="0"
/>
</h3>
</div>
</template>
<template #columns="scope">
<div class="key">
<input
v-model="scope.row.value"
:placeholder="t('harvester.subnet.allowSubnet.placeholder')"
/>
</div>
</template>
</ArrayList>
<ArrayList
v-model:value="value.spec.excludeIps"
:show-header="true"
class="mt-20"
:mode="mode"
:add-label="t('harvester.setting.storageNetwork.exclude.addIp')"
>
<template #column-headers>
<div class="box">
<h3 class="key">
{{ t('harvester.setting.storageNetwork.exclude.label') }}
<i
v-clean-tooltip="{content: excludeIPsTooltip, triggers: ['hover', 'touch', 'focus'] }"
v-stripped-aria-label="excludeIPsTooltip"
class="icon icon-info"
tabindex="0"
/>
</h3>
</div>
</template>
<template #columns="scope">
<div class="key">
<input
v-model="scope.row.value"
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
/>
</div>
</template>
</ArrayList>
</Tab>
</ResourceTabs>
</CruResource>
</template>
@@ -0,0 +1,162 @@
<script>
import debounce from 'lodash/debounce';
import { _EDIT, _VIEW } from '@shell/config/query-params';
import { removeAt } from '@shell/utils/array';
export default {
name: 'StaticRoutes',
emits: ['update:value'],
props: {
value: {
type: Array,
default: null,
},
mode: {
type: String,
default: _EDIT,
},
},
data() {
const rows = (this.value || []).map((row) => {
return {
cidr: row.cidr || '',
nextHopIP: row.nextHopIP || '',
};
});
return { rows };
},
computed: {
isView() {
return this.mode === _VIEW;
},
showAdd() {
return !this.isView;
},
showRemove() {
return !this.isView;
},
nextHopIPTooltip() {
return this.t('harvester.vpc.staticRoutes.nextHopIP.tooltip');
},
},
created() {
this.queueUpdate = debounce(this.update, 100);
},
methods: {
add() {
this.rows.push({
cidr: '',
nextHopIP: '',
});
this.queueUpdate();
},
remove(idx) {
removeAt(this.rows, idx);
this.queueUpdate();
},
update() {
if (this.isView) {
return;
}
this.$emit('update:value', this.rows);
}
},
};
</script>
<template>
<div>
<div
v-if="rows.length"
class="static-route-row"
>
<div
v-for="(row, idx) in rows"
:key="idx"
>
<div class="pool-headers cidr">
<span class="pool-cidr">
<t k="harvester.vpc.staticRoutes.cidr.label" />
</span>
<span class="pool-nextHopIP">
<t k="harvester.vpc.staticRoutes.nextHopIP.label" />
<i
v-clean-tooltip="{content: nextHopIPTooltip, triggers: ['hover', 'touch', 'focus'] }"
v-stripped-aria-label="nextHopIPTooltip"
class="icon icon-info"
tabindex="0"
/>
</span>
</div>
<div class="pool-row cidr">
<div class="pool-cidr">
<span v-if="isView">
{{ row.cidr }}
</span>
<input
v-else
v-model="row.cidr"
type="text"
:placeholder="t('harvester.vpc.staticRoutes.cidr.placeholder')"
@input="queueUpdate"
/>
</div>
<div class="pool-nextHopIP">
<span v-if="isView">
{{ row.nextHopIP }}
</span>
<input
v-else
v-model="row.nextHopIP"
type="text"
:placeholder="t('harvester.vpc.staticRoutes.nextHopIP.placeholder')"
@input="queueUpdate"
/>
</div>
<button
v-if="showRemove"
type="button"
class="btn role-link pl-0"
@click="remove(idx)"
>
<t k="generic.remove" />
</button>
</div>
</div>
</div>
<button
v-if="showAdd"
type="button"
class="btn role-tertiary add"
@click="add()"
>
<t k="generic.add" />
</button>
</div>
</template>
<style lang="scss" scoped>
.pool-headers, .pool-row {
display: grid;
grid-column-gap: $column-gutter;
margin-bottom: 10px;
align-items: center;
&.cidr {
grid-template-columns: 40%+$column-gutter 40%+$column-gutter 15%;
}
}
</style>
@@ -0,0 +1,187 @@
<script>
import debounce from 'lodash/debounce';
import { _EDIT, _VIEW } from '@shell/config/query-params';
import { removeAt } from '@shell/utils/array';
import { HCI } from '../../types';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default {
name: 'VpcPeerings',
emits: ['update:value'],
components: { LabeledSelect },
props: {
value: {
type: Array,
default: null,
},
mode: {
type: String,
default: _EDIT,
},
vpc: {
type: Object,
default: () => ({}),
},
},
async fetch() {
await this.$store.dispatch('harvester/findAll', { type: HCI.VPC });
},
data() {
const rows = (this.value || []).map((row) => {
return {
localConnectIP: row.localConnectIP || '',
remoteVpc: row.remoteVpc || '',
};
});
return { rows };
},
computed: {
isView() {
return this.mode === _VIEW;
},
isEdit() {
return this.mode === _EDIT;
},
showAdd() {
return !this.isView;
},
showRemove() {
return !this.isView;
},
remoteVpcOptions() {
const allVpcs = this.$store.getters['harvester/all'](HCI.VPC) || [];
// filter self vpc if editing
const vpcs = this.isEdit ? allVpcs.filter((v) => v.id !== this.vpc.id) : allVpcs;
return vpcs.map((n) => ({
label: n.id,
value: n.id,
}));
},
localConnectIPTooltip() {
return this.t('harvester.vpc.vpcPeerings.localConnectIP.tooltip');
},
},
created() {
this.queueUpdate = debounce(this.update, 100);
},
methods: {
add() {
this.rows.push({
localConnectIP: '',
remoteVpc: '',
});
this.queueUpdate();
},
remove(idx) {
removeAt(this.rows, idx);
this.queueUpdate();
},
update() {
if (this.isView) {
return;
}
this.$emit('update:value', this.rows);
}
},
};
</script>
<template>
<div>
<div
v-if="rows.length"
class="static-route-row"
>
<div
v-for="(row, idx) in rows"
:key="idx"
>
<div class="pool-headers localConnectIP">
<span class="pool-localConnectIP">
<t k="harvester.vpc.vpcPeerings.localConnectIP.label" />
<i
v-clean-tooltip="{content: localConnectIPTooltip, triggers: ['hover', 'touch', 'focus'] }"
v-stripped-aria-label="localConnectPTooltip"
class="icon icon-info"
tabindex="0"
/>
</span>
<span class="pool-remoteVpc">
<t k="harvester.vpc.vpcPeerings.remoteVpc.label" />
</span>
</div>
<div class="pool-row localConnectIP">
<div class="pool-localConnectIP">
<span v-if="isView">
{{ row.localConnectIP }}
</span>
<input
v-else
v-model="row.localConnectIP"
type="text"
:placeholder="t('harvester.vpc.vpcPeerings.localConnectIP.placeholder')"
@input="queueUpdate"
/>
</div>
<div class="pool-remoteVpc">
<span v-if="isView">
{{ row.remoteVpc }}
</span>
<LabeledSelect
v-model:value="row.remoteVpc"
:options="remoteVpcOptions"
:mode="mode"
@update:value="queueUpdate"
/>
</div>
<button
v-if="showRemove"
type="button"
class="btn role-link pl-0"
@click="remove(idx)"
>
<t k="generic.remove" />
</button>
</div>
</div>
</div>
<button
v-if="showAdd"
type="button"
class="btn role-tertiary add"
@click="add()"
>
<t k="generic.add" />
</button>
</div>
</template>
<style lang="scss" scoped>
.pool-headers, .pool-row {
display: grid;
grid-column-gap: $column-gutter;
margin-bottom: 10px;
align-items: center;
&.localConnectIP {
grid-template-columns: 40%+$column-gutter 40%+$column-gutter 15%;
}
}
</style>
+120
View File
@@ -0,0 +1,120 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import Tab from '@shell/components/Tabbed/Tab';
import Loading from '@shell/components/Loading';
import CreateEditView from '@shell/mixins/create-edit-view';
import ResourceTabs from '@shell/components/form/ResourceTabs/index';
import StaticRoutes from './StaticRoutes';
import VpcPeerings from './VpcPeerings';
import { set } from '@shell/utils/object';
export default {
name: 'EditVPC',
emits: ['update:value'],
components: {
CruResource,
NameNsDescription,
Tab,
StaticRoutes,
ResourceTabs,
Loading,
VpcPeerings
},
mixins: [CreateEditView],
inheritAttrs: false,
data() {
set(this.value, 'spec', this.value.spec || {
staticRoutes: [],
vpcPeerings: [],
});
return { staticRoutes: [] };
},
methods: {
async saveVpc(buttonCb) {
const errors = [];
try {
const name = this.value?.metadata?.name;
if (!name) {
errors.push(this.t('validation.required', { key: this.t('generic.name') }, true));
}
if (errors.length > 0) {
buttonCb(false);
this.errors = errors;
return false;
}
await this.value.save();
buttonCb(true);
this.done();
} catch (e) {
this.errors = [e];
buttonCb(false);
}
},
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<CruResource
v-else
:done-route="doneRoute"
:resource="value"
:mode="mode"
:apply-hooks="applyHooks"
:errors="errors"
@finish="saveVpc"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:mode="mode"
:side-tabs="true"
>
<Tab
name="staticRoutes"
:label="t('harvester.vpc.staticRoutes.label')"
:weight="-1"
class="bordered-table"
>
<StaticRoutes
v-model:value="value.spec.staticRoutes"
class="col span-12"
:mode="mode"
/>
</Tab>
<Tab
name="vpcPeerings"
:label="t('harvester.vpc.vpcPeerings.label')"
:weight="-2"
class="bordered-table"
>
<VpcPeerings
v-model:value="value.spec.vpcPeerings"
class="col span-12"
:mode="mode"
:vpc="value"
/>
</Tab>
</ResourceTabs>
</CruResource>
</template>
@@ -20,6 +20,10 @@ export default {
type: String,
default: 'create'
},
osType: {
type: String,
default: ''
},
value: {
type: String,
default: ''
@@ -64,6 +68,12 @@ export default {
},
watch: {
osType(neu) {
if (neu === 'windows') {
this.id = '';
}
},
value(neu) {
this.yamlScript = neu;
},
@@ -28,6 +28,10 @@ export default {
type: String,
default: ''
},
osType: {
type: String,
default: ''
},
userScript: {
type: String,
default: ''
@@ -203,6 +207,7 @@ export default {
<DataTemplate
ref="userTemplate"
:value="userScript"
:os-type="osType"
type="user"
:view-code="viewCode"
:mode="mode"
@@ -219,6 +224,7 @@ export default {
ref="networkTemplate"
:value="networkScript"
type="network"
:os-type="osType"
:view-code="viewCode"
:mode="mode"
:config-id="configNetworkId"
@@ -2,23 +2,44 @@
import UnitInput from '@shell/components/form/UnitInput';
import InputOrDisplay from '@shell/components/InputOrDisplay';
import { GIBIBYTE } from '../../utils/unit';
import { Checkbox } from '@components/Form/Checkbox';
import { _VIEW } from '@shell/config/query-params';
import { HCI } from '../../types';
import { allHash } from '@shell/utils/promise';
import { HCI_SETTING } from '../../config/settings';
const DEFAULT_HOT_PLUG_TIMES = 4;
export default {
name: 'HarvesterEditCpuMemory',
emits: ['updateCpuMemory'],
components: { UnitInput, InputOrDisplay },
components: {
UnitInput, InputOrDisplay, Checkbox
},
props: {
cpu: {
type: Number,
default: null
},
maxCpu: {
type: Number,
default: null
},
memory: {
type: String,
default: null
},
maxMemory: {
type: String,
default: null
},
enableHotPlug: {
type: Boolean,
default: false
},
mode: {
type: String,
default: 'create',
@@ -29,21 +50,59 @@ export default {
}
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const res = await allHash({ settings: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }) });
this.settings = res.settings || [];
},
data() {
return {
GIBIBYTE,
localCpu: this.cpu,
localMemory: this.memory
localCpu: this.cpu,
localMemory: this.memory,
maxLocalCpu: this.maxCpu,
maxLocalMemory: this.maxMemory,
localEnableHotPlug: this.enableHotPlug,
settings: []
};
},
computed: {
cupDisplay() {
isView() {
return this.mode === _VIEW;
},
cpuDisplay() {
return `${ this.localCpu } C`;
},
maxCpuDisplay() {
return `${ this.maxLocalCpu } C`;
},
memoryDisplay() {
return `${ this.localMemory }`;
},
maxMemoryDisplay() {
return `${ this.maxLocalMemory }`;
},
cpuMemoryHotplugTooltip() {
return this.t('harvester.virtualMachine.hotplug.tooltip', { hotPlugTimes: this.maxHotplugRatio });
},
isCPUMemoryHotPlugFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('cpuMemoryHotplug');
},
maxHotplugRatio() {
const maxHotPlugRatioSetting = this.settings.find((s) => s.id === HCI_SETTING.MAX_HOTPLUG_RATIO);
const maxPlugRatio = maxHotPlugRatioSetting?.value ? parseInt(maxHotPlugRatioSetting.value, 10) : DEFAULT_HOT_PLUG_TIMES;
return maxPlugRatio;
}
},
@@ -55,71 +114,161 @@ export default {
if (neu && !neu.includes('null')) {
this.localMemory = neu;
}
}
},
maxCpu(neu) {
this.maxLocalCpu = neu;
},
maxMemory(neu) {
if (neu && !neu.includes('null')) {
this.maxLocalMemory = neu;
}
},
enableHotPlug(neu) {
this.localEnableHotPlug = neu;
},
},
methods: {
change() {
let memory = '';
if (String(this.localMemory).includes('Gi')) {
memory = this.localMemory;
hotPlugChanged(neu) {
// If hot plug is enabled, we need to update the maxCpu and maxMemory values
if (neu) {
this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null;
this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null;
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, neu);
} else {
memory = `${ this.localMemory }${ GIBIBYTE }`;
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, '', null, neu);
}
if (memory.includes('null')) {
memory = null;
}
this.$emit('updateCpuMemory', this.localCpu, memory);
},
changeMemory() {
if (this.localEnableHotPlug) {
this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null;
}
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
},
changeCPU() {
if (this.localEnableHotPlug) {
this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null;
}
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
},
changeMaxCPUMemory() {
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
},
}
};
</script>
<template>
<div class="row">
<div class="col span-6 mb-10">
<InputOrDisplay
name="CPU"
:value="cupDisplay"
:mode="mode"
>
<UnitInput
v-model:value="localCpu"
label="CPU"
suffix="C"
:delay="0"
required
:disabled="disabled"
<div>
<div class="row">
<div class="col span-6 mb-10">
<InputOrDisplay
name="CPU"
:value="cpuDisplay"
:mode="mode"
class="mb-20"
@update:value="change"
/>
</InputOrDisplay>
class="mb-10"
>
<UnitInput
v-model:value="localCpu"
label="CPU"
suffix="C"
:delay="0"
required
:disabled="disabled"
:mode="mode"
class="mb-20"
@update:value="changeCPU"
/>
</InputOrDisplay>
</div>
<div class="col span-6 mb-10">
<InputOrDisplay
:name="t('harvester.virtualMachine.input.memory')"
:value="memoryDisplay"
:mode="mode"
>
<UnitInput
v-model:value="localMemory"
:label="t('harvester.virtualMachine.input.memory')"
:mode="mode"
:input-exponent="3"
:delay="0"
:increment="1024"
:output-modifier="true"
:disabled="disabled"
required
:suffix="GIBIBYTE"
class="mb-20"
@update:value="changeMemory"
/>
</InputOrDisplay>
</div>
</div>
<div class="col span-6 mb-10">
<InputOrDisplay
:name="t('harvester.virtualMachine.input.memory')"
:value="memoryDisplay"
:mode="mode"
>
<UnitInput
v-model:value="localMemory"
:label="t('harvester.virtualMachine.input.memory')"
<div
v-if="isCPUMemoryHotPlugFeatureEnabled"
class="row"
>
<Checkbox
v-model:value="localEnableHotPlug"
class="check"
type="checkbox"
:label="t('harvester.virtualMachine.hotplug.title')"
:disabled="isView"
@update:value="hotPlugChanged"
/>
<i
v-clean-tooltip="{content: cpuMemoryHotplugTooltip, triggers: ['hover', 'touch', 'focus'] }"
v-stripped-aria-label="cpuMemoryHotplugTooltip"
class="icon icon-info"
tabindex="0"
/>
</div>
<div
v-if="localEnableHotPlug && isCPUMemoryHotPlugFeatureEnabled"
class="row"
>
<div class="col span-6 mb-10">
<InputOrDisplay
:name="t('harvester.virtualMachine.input.maxCpu')"
:value="maxCpuDisplay"
:mode="mode"
:input-exponent="3"
:delay="0"
:increment="1024"
:output-modifier="true"
:disabled="disabled"
required
:suffix="GIBIBYTE"
class="mb-20"
@update:value="change"
/>
</InputOrDisplay>
class="mt-20"
>
<UnitInput
v-model:value="maxLocalCpu"
:label="t('harvester.virtualMachine.input.maxCpu')"
suffix="C"
:delay="0"
:disabled="disabled"
:mode="mode"
class="mt-20"
@update:value="changeMaxCPUMemory"
/>
</InputOrDisplay>
</div>
<div class="col span-6 mb-10">
<InputOrDisplay
:name="t('harvester.virtualMachine.input.maxMemory')"
:value="maxMemoryDisplay"
:mode="mode"
class="mt-20"
>
<UnitInput
v-model:value="maxLocalMemory"
:label="t('harvester.virtualMachine.input.maxMemory')"
:mode="mode"
:input-exponent="3"
:delay="0"
:increment="1024"
:output-modifier="true"
:disabled="disabled"
:suffix="GIBIBYTE"
class="mt-20"
@update:value="changeMaxCPUMemory"
/>
</InputOrDisplay>
</div>
</div>
</div>
</template>
@@ -17,6 +17,7 @@ import { SOURCE_TYPE } from '../../../config/harvester-map';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
import { HCI } from '../../../types';
import { VOLUME_MODE } from '@pkg/harvester/config/types';
import { OFF } from '../../../models/kubevirt.io.virtualmachine';
export default {
emits: ['update:value'],
@@ -266,7 +267,17 @@ export default {
isLonghornV2(volume) {
return volume?.pvc?.isLonghornV2 || volume?.pvc?.storageClass?.isLonghornV2;
}
},
isResizeDisabled(volume) {
if (this.isCreate) return false;
if (volume.newCreateId) return false;
const isStopped = this.vm.stateDisplay === OFF;
const isLonghornV2 = this.isLonghornV2(volume);
return !isStopped || isLonghornV2;
},
},
};
</script>
@@ -347,6 +358,7 @@ export default {
:is-edit="isEdit"
:is-view="isView"
:is-virtual-type="isVirtualType"
:is-resize-disabled="isResizeDisabled(rows[i])"
:mode="mode"
:idx="i"
:validate-required="validateRequired"
@@ -8,6 +8,7 @@ import { Banner } from '@components/Banner';
import { PVC } from '@shell/config/types';
import { formatSi, parseSi } from '@shell/utils/units';
import { HCI } from '../../../../types';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
import { _VIEW } from '@shell/config/query-params';
import LabelValue from '@shell/components/LabelValue';
@@ -63,13 +64,19 @@ export default {
isVirtualType: {
type: Boolean,
default: true
}
},
isResizeDisabled: {
type: Boolean,
default: false
},
},
data() {
return {
GIBIBYTE,
VOLUME_TYPE,
HCI_ANNOTATIONS,
InterfaceOption,
loading: false,
images: [],
@@ -90,12 +97,23 @@ export default {
},
imagesOption() {
return this.images.filter((c) => c.isReady).sort((a, b) => a.creationTimestamp > b.creationTimestamp ? -1 : 1).map( (I) => {
return {
label: this.imageOptionLabel(I),
value: I.id
};
});
return this.images
.filter((image) => {
if (!image.isReady) return false;
// exclude internal images created during upgrade
const isInternalCreatedImage =
image.namespace === 'harvester-system' &&
image.labels?.[HCI_ANNOTATIONS.UPGRADE];
return !isInternalCreatedImage;
})
.sort((a, b) => (a.creationTimestamp > b.creationTimestamp ? -1 : 1))
.map((image) => ({
label: this.imageOptionLabel(image),
value: image.id,
disabled: image.isImportedImage
}));
},
imageName() {
@@ -122,10 +140,6 @@ export default {
return this.$store.getters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
},
isLonghornV2() {
return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.isLonghornV2;
},
selectedImage() {
return this.$store.getters['harvester/all'](HCI.IMAGE)?.find( (I) => this.value.image === I.id);
},
@@ -337,7 +351,7 @@ export default {
:label="t('harvester.fields.size')"
:mode="mode"
:required="validateRequired"
:disable="isLonghornV2"
:disabled="isResizeDisabled"
:suffix="GIBIBYTE"
@update:value="update"
/>
@@ -13,6 +13,7 @@ import { ucFirst } from '@shell/utils/string';
import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storageclass';
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
import { GIBIBYTE } from '../../../../utils/unit';
import { isInternalStorageClass } from '../../../../utils/storage-class';
import { VOLUME_MODE } from '@pkg/harvester/config/types';
export default {
@@ -55,7 +56,12 @@ export default {
isVirtualType: {
type: Boolean,
default: true
}
},
isResizeDisabled: {
type: Boolean,
default: false
},
},
data() {
@@ -118,19 +124,22 @@ export default {
storageClassOptions() {
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
let disabled = false;
if (isInternalStorageClass(s.name)) {
label += ` (${ this.t('harvester.storage.internal.label') })`;
disabled = true;
}
return {
label,
value: s.name,
disabled
};
}) || [];
},
isLonghornV2() {
return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.isLonghornV2;
},
isLonghornStorageClass() {
const selectedSC = this.storageClasses.find((sc) => sc.name === this.value?.storageClassName) || {};
@@ -302,7 +311,7 @@ export default {
:mode="mode"
:required="validateRequired"
:label="t('harvester.fields.size')"
:disabled="isLonghornV2"
:disabled="isResizeDisabled"
:suffix="GIBIBYTE"
@update:value="update"
/>
@@ -114,13 +114,11 @@ export default {
},
machineTypeOptions() {
return [{
label: 'None',
value: ''
}, {
label: 'q35',
value: 'q35'
}];
return this.machineTypes.map((type) => {
if (!type) return { label: 'None', value: '' };
return { label: type, value: type };
});
},
templateOptions() {
@@ -239,6 +237,9 @@ export default {
}
const versions = await this.$store.dispatch('harvester/findAll', { type: HCI.VM_VERSION });
const curVersion = versions.find( (V) => V.id === id);
if (!curVersion?.spec?.vm) return;
const cloneVersionVM = clone(curVersion.spec.vm);
delete cloneVersionVM.spec?.template?.spec?.accessCredentials;
@@ -246,7 +247,6 @@ export default {
delete cloneVersionVM.spec?.template?.metadata?.annotations?.[HCI_ANNOTATIONS.DYNAMIC_SSHKEYS_USERS];
const claimTemplate = parseVolumeClaimTemplates(cloneVersionVM);
const deleteDataSource = claimTemplate.map((volume) => {
if (volume?.spec?.dataSource) {
delete volume.spec.dataSource;
@@ -257,6 +257,17 @@ export default {
cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
// Update instance labels, labels and annotations
this.value.spec.template.metadata.labels = cloneVersionVM.spec.template.metadata.labels || {};
this.value.metadata.labels = {
...(curVersion.metadata?.labels || {}),
...(cloneVersionVM.metadata?.labels || {})
};
this.value.metadata.annotations = {
...(curVersion.metadata?.annotations || {}),
...(cloneVersionVM.metadata?.annotations || {})
};
this.getInitConfig({
value: cloneVersionVM, existUserData: true, fromTemplate: true
});
@@ -327,6 +338,15 @@ export default {
saveVM(buttonCb) {
clear(this.errors);
this.validateCPUMemory();
// block create VM flow if has validation errors
if (this.errors.length) {
buttonCb(false);
return;
}
if (this.isSingle) {
this.saveSingle(buttonCb);
} else {
@@ -334,6 +354,18 @@ export default {
}
},
validateCPUMemory() {
const { cpu, memory } = this;
if ((!cpu)) {
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.cpu') }, true));
}
if ((!memory)) {
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.memory') }, true));
}
},
async saveSingle(buttonCb) {
this.parseVM();
this.value.spec.template.spec.hostname = this.hostname ? this.hostname : this.value.metadata.name;
@@ -519,6 +551,7 @@ export default {
:apply-hooks="applyHooks"
@finish="saveVM"
@cancel="cancelAction"
@error="e=>errors=e"
>
<RadioGroup
v-if="isCreate"
@@ -591,9 +624,13 @@ export default {
:label="t('harvester.virtualMachine.detail.tabs.basics')"
>
<CpuMemory
class="mb-20"
:cpu="cpu"
:max-cpu="maxCpu"
:memory="memory"
:max-memory="maxMemory"
:mode="mode"
:enable-hot-plug="cpuMemoryHotplugEnabled"
@updateCpuMemory="updateCpuMemory"
/>
@@ -761,10 +798,31 @@ export default {
/>
</Tab>
<Tab
name="annotations"
:label="t('harvester.tab.annotations')"
:weight="-11"
>
<Banner color="info">
<t k="harvester.virtualMachine.annotations.banner" />
</Banner>
<KeyValue
key="annotations"
:value="value.annotations"
:protected-keys="value.systemAnnotations || []"
:toggle-filter="toggler"
:add-label="t('labels.addAnnotation')"
:mode="mode"
:read-allowed="false"
:value-can-be-empty="true"
@update:value="value.setAnnotations($event)"
/>
</Tab>
<Tab
name="advanced"
:label="t('harvester.tab.advanced')"
:weight="-11"
:weight="-12"
>
<div class="row mb-20">
<div class="col span-6">
@@ -862,6 +920,7 @@ export default {
ref="yamlEditor"
:user-script="userScript"
:mode="mode"
:os-type="osType"
:view-code="isWindows"
:namespace="value.metadata.namespace"
:network-script="networkScript"
@@ -81,6 +81,7 @@ export default {
:apply-hooks="applyHooks"
:yaml-modifiers="yamlModifiers"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
@@ -235,6 +235,7 @@ export default {
:apply-hooks="applyHooks"
:yaml-modifiers="yamlModifiers"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
@@ -213,7 +213,7 @@ export default {
:subtypes="[]"
:can-yaml="false"
:validation-passed="fvFormIsValid"
@error="e=>errors = e"
@error="e=>errors=e"
@finish="save"
@cancel="done"
>
@@ -67,6 +67,7 @@ export default {
:mode="mode"
:errors="errors"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
@@ -373,6 +373,7 @@ export default {
:mode="mode"
:errors="errors"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
@@ -1,4 +1,5 @@
<script>
import { NORMAN } from '@shell/config/types';
import { HCI } from '../types';
export default {
@@ -13,10 +14,24 @@ export default {
const harvesterSettings = await this.$store.dispatch('harvester/findAll', { type: HCI.SETTING });
this.harvesterSettings = harvesterSettings;
const clusterRoleTemplateBindingSchema = this.$store.getters['rancher/schemaFor'](NORMAN.CLUSTER_ROLE_TEMPLATE_BINDING);
if (clusterRoleTemplateBindingSchema) {
const normanBindings = await this.$store.dispatch('rancher/findAll', { type: NORMAN.CLUSTER_ROLE_TEMPLATE_BINDING }, { root: true });
this.clusterRoleTemplateBinding = normanBindings;
}
},
data() {
return { harvesterSettings: [] };
const user = this.$store.getters['auth/v3User'];
return {
harvesterSettings: [],
clusterRoleTemplateBinding: [],
user,
};
},
computed: {
@@ -31,19 +46,28 @@ export default {
} catch (e) {}
return isMatch;
},
isClusterOwner() {
const template = this.clusterRoleTemplateBinding.find((template) => {
return template.userId === this.user?.id;
});
return template?.roleTemplateId === 'cluster-owner';
},
shouldShowError() {
return this.isClusterOwner && !this.isMatch && this.value;
}
}
};
</script>
<template>
<div v-if="isMatch">
{{ value }}
</div>
<div v-else>
<div>
{{ value }}
<p
v-if="value"
v-if="shouldShowError"
class="text-error"
>
{{ t('harvester.backup.matchTarget') }}
@@ -35,11 +35,26 @@ export default {
},
computed: {
// Return VM instance IP and VM annotation IP
ips() {
return [...this.vmiIp, ...this.networkAnnotationIP]
.filter(Boolean)
.sort((a, b) => a.ip < b.ip ? -1 : 1);
if (this.vmiIp.length) {
return [...this.vmiIp, ...this.networkAnnotationIP]
.filter(Boolean)
.sort((a, b) => a.ip < b.ip ? -1 : 1);
}
return this.customAnnotationIP;
},
customAnnotationIP() {
const annotationIp = get(this.row, `metadata.annotations."${ HCI_ANNOTATIONS.CUSTOM_IP }"`);
if (annotationIp && isIpv4(annotationIp)) {
return [{
name: 'custom-ip', ip: annotationIp, isCustom: true
}];
}
return [];
},
networkAnnotationIP() {
@@ -97,12 +112,13 @@ export default {
<template>
<div v-if="showIP">
<span
v-for="{ip, name} in ips"
:key="ip"
v-for="{ ip, name, isCustom } in ips"
:key="`${ip}-${name}`"
>
<CopyToClipboardText
v-clean-tooltip="name"
v-clean-tooltip="isCustom ? t('harvester.formatters.harvesterIpAddress.customIpTooltip') : name"
:text="ip"
:plain="isCustom"
/>
</span>
</div>
+163 -10
View File
@@ -49,9 +49,13 @@ dialog:
}
protip: "Tip: Hold the {alternateLabel} key while clicking action to bypass this confirmation"
stop:
message: "Are you sure you want to continue stop the {type} {names}?"
message: "Are you sure you want to stop the {type} {names}?"
pause:
message: "Are you sure you want to continue pause the {type} {names}?"
message: "Are you sure you want to pause the {type} {names}?"
restart:
message: "Are you sure you want to restart the {type} {names}?"
softreboot:
message: "Are you sure you want to soft reboot the {type} {names}?"
promptRemove:
title: "Delete {type}"
warningMessage: "Deleting the selected {type} permanently removes all resources on {thisOrThese} {type}. This action is irreversible. Do you want to continue?"
@@ -66,7 +70,6 @@ harvester:
tip: 'Upload an icon to replace the Harvester favicon in the browser tab. Max file size is 20KB'
productLabel: 'Harvester'
modal:
backup:
success: 'Backup { backUpName } has been initiated.'
addBackup: Add Backup
@@ -133,13 +136,31 @@ harvester:
delete: Delete
bundle:
title: Generate a Support Bundle
titleDescription: Collect system-related logs in Harvester to assist with troubleshooting and support.
tip: Each field below specifies an aspect of the support bundle. For detailed explanations, please refer to the <a href="{doc}" target="_blank">documentation</a>.
url: Issue URL
description: Description
requiredDesc: Description is required!
titleDescription: Collect system-related logs in Harvester to help with troubleshooting and support.
namespaces:
label: Namespaces
tooltip: Select additional namespaces to include in the support bundle.<br/>See docs support-bundle-namespaces for detail.
selectAll: 'Select All'
unselectAll: 'Unselect All'
timeout:
label: Timeout
tooltip: Minutes allows for completion of the support bundle generation process.<br/>See docs support-bundle-timeout for detail.
expiration:
label: Expiration
tooltip: Minutes before deleting packaged but not downloaded support bundle.<br/>See docs support-bundle-expiration for detail.
nodeTimeout:
label: Node Collection Timeout
tooltip: Minutes allowed for collecting logs/configurations on nodes.<br/>See docs support-bundle-node-collection-timeout for detail.
hotplug:
success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
title: Add Volume
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:
@@ -196,6 +217,7 @@ harvester:
suspendSchedule: Suspend
restoreExistingVM: Replace Existing
migrate: Migrate
cpuAndMemoryHotplug: Edit CPU and Memory
abortMigration: Abort Migration
createTemplate: Generate Template
enableMaintenance: Enable Maintenance Mode
@@ -229,6 +251,8 @@ harvester:
{count, plural,
=1 {core}
other {cores}}
harvesterIpAddress:
customIpTooltip: "Custom IP (set via annotation)"
tableHeaders:
imageEncryption: Encryption
size: Size
@@ -276,6 +300,7 @@ harvester:
quotas: Quotas
snapshots: Snapshots
instanceLabel: Instance Labels
annotations: Annotations
fields:
version: Version
name: Name
@@ -376,6 +401,8 @@ harvester:
ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
hash:
sha512: 'Invalid SHA512 checksum.'
subnet:
excludeIps: 'Exclude IPs cannot be empty. Please remove or fill in the exclude IPs.'
dashboard:
label: Dashboard
@@ -549,6 +576,10 @@ harvester:
virtualMachine:
label: Virtual Machines
osType: OS Type
hotplug:
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
instance:
singleInstance:
multipleInstance:
@@ -674,7 +705,7 @@ harvester:
network:
label: Network Data Template
title: "Network Data:"
tip: "The network-data configuration allows you to customize the instance’s networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>"
tip: "The network-data configuration allows you to customize the instance's networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>"
scheduling:
affinity:
anyNode: 'Run virtual machine on any available node'
@@ -694,6 +725,9 @@ harvester:
invalidUser: Invalid Username.
input:
name: Name
cpu: CPU
maxCpu: Maximum CPU
maxMemory: Maximum Memory
memory: Memory
image: Image
sshKey: SSHKey
@@ -754,6 +788,7 @@ harvester:
dedicatedResources: Dedicated Resources
down: Virtual machine not running
affinityRules: Affinity Rules
phase: Phase
sourceNode: Source Node
targetNode: Target Node
started: Started
@@ -791,6 +826,8 @@ harvester:
banner: These labels are automatically synchronized to the virtual machine instance.
labels:
banner: These key values are added as labels to the virtual machine.
annotations:
banner: These key values are added as annotations to the virtual machine.
volume:
label: Volumes
@@ -855,6 +892,8 @@ harvester:
uploading: "{name} is uploading, please do not refresh or close the page."
checksum: Checksum
checksumTip: Validate the image using the SHA512 checksum, if specified.
tooltip:
imported: Created automatically by the vm-import-controller
vmTemplate:
label: Templates
@@ -891,7 +930,7 @@ harvester:
releaseTip: Please read the upgrade documentation carefully. You can view details on the <a href="{url}" target="_blank">Harvester Release Notes</a>.
checkReady: I have read and understood the upgrade instructions related to this Harvester version.
pending: Pending
upload:
upload:
duplicatedFile: The file you are trying to upload already exists.
repoInfo:
upgradeStatus: Upgrade Status
@@ -990,6 +1029,57 @@ harvester:
progress: Restore in progress
complete: Restore completed
subnet:
cidrBlock:
tooltip: The subnet range in CIDR notation. Note that the CIDR blocks of different Subnets' within the same VPC cannot overlap.
label: CIDR Block
placeholder: e.g. 172.20.0.0/16
protocol:
label: Protocol
provider:
tooltip: Network provider for this Subnet. Must be one of the Virtual Machine Networks in OverlayNetwork type.
label: Provider
vpc:
label: Virtual Private Cloud
gateway:
label: Gateway IP
placeholder: e.g. 172.20.0.1
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.
allowSubnet:
label: Allow Subnets
tooltip: You can specify certain subnets which can communicate with this subnet.
addSubnet: Add Allow Subnet
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.
vpc:
noAddonEnabled:
prefix: The kubeovn-operator add-on is not enabled, click
middle: here
suffix: to enable the add-on to successfully create VPC and subnet.
label: Virtual Private Cloud
noChild: There is no subnet defined in this Virtual Private Cloud.
createSubnet: Create Subnet
staticRoutes:
label: Static Routes
cidr:
label: CIDR
placeholder: e.g. 172.16.0.0/16
nextHopIP:
tooltip: The localConnectIP on the other end of the peering VPC.
label: Next Hop IP
placeholder: e.g. 169.254.0.2
vpcPeerings:
label: VPC Peerings
localConnectIP:
tooltip: The designated IP address in CIDR notation of the VPC peering endpoint. Note that both IP addresses (for the VPC and the other VPC) should belong to the same subnet range and should not conflict with the cidrBlock in any existing subnets.
label: Local Connect IP
placeholder: e.g. 169.254.0.1/16
remoteVpc:
label: Remote VPC
network:
label: Virtual Machine Networks
tabs:
@@ -1032,6 +1122,7 @@ harvester:
accessKeyId: Specify your access key ID
secretAccessKey: Specify your secret access key
cert: Upload a self-signed SSL certificate
noSearchMatch: No settings match your search.
vlanChangeTip: The newly modified default network interface only applies to newly added nodes, not existing ones.
defaultPhysicalNIC: Default Network Interface
modifiedMessage: Settings that have been customized from default settings are tagged with 'Modified'.
@@ -1045,6 +1136,9 @@ harvester:
publicCertificate: Public Certificate
privateKey: Private Key
ca: CA
rancherCluster:
kubeConfig: Rancher KubeConfig
removeUpstreamClusterWhenNamespaceIsDeleted: Remove Upstream Cluster When Namespace Is Deleted
storageNetwork:
range:
placeholder: e.g. 172.16.0.0/24
@@ -1053,14 +1147,28 @@ harvester:
clusterNetwork: Cluster Network
vlan: VLAN ID
exclude:
label: Exclude
placeholder: e.g. 172.16.0.1/32
label: Exclude IPs
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.'
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
vmForceDeletionPolicy:
period: Period
vmMigrationNetwork:
parseError: "Failed to parse existing configuration."
fetchError: "Failed to load required network resources: {error}. Please refresh the page or try again later."
clusterNetwork: Cluster Network
vlan: VLAN ID
range:
placeholder: e.g. 172.16.0.0/24
label: IP Range
invalid: '"Range" is invalid.'
exclude:
label: Excluded IPs
placeholder: e.g. 172.16.0.1/32
invalid: '"Exclude list" is invalid.'
addButton: Add Exclude IP
ratio : Ratio
autoRotateRKE2Certs:
expiringInHours: Expiring in
@@ -1074,6 +1182,11 @@ harvester:
provisioner: Provisioner
volumeSnapshotClassName: Volume Snapshot Class Name
backupVolumeSnapshotClassName: Backup Volume Snapshot Class Name
csiOnlineExpandValidation:
provisioner: Provisioner
value: Value
invalidJsonFormat: "Configuration value is not a valid JSON format: {error}"
failedToLoadDrivers: "Failed to load CSI drivers. Error: {error}"
containerdRegistry:
mirrors:
mirrors: Mirrors
@@ -1104,6 +1217,10 @@ harvester:
uploadSuccess: "{name} uploaded successfully. Press Upgrade button to start the cluster upgrade process."
deleteImage: Please select an image to delete.
deleteSuccess: "{name} deleted successfully."
imagePreloadStrategy: Image Preload Strategy
restoreVM: Restore VM
strategyType: Strategy Type
concurrency: Concurrency
harvesterMonitoring:
label: Harvester Monitoring
section:
@@ -1265,6 +1382,32 @@ harvester:
allowedTopologies:
title: Allowed Topologies
tooltip: Allowed Topologies helps scheduling virtual machines on hosts which match all of below expressions.
internal:
label: Internal Storage Class
cannotDeleteTooltip: Internal storage class volumes cannot be deleted
cannotDeleteOrDefaultTooltip: Internal storage classes cannot be deleted or set as default
cdiSettings:
label: CDI Settings
volumeModeAccessModes:
label: Volume Mode / Access Modes
tooltip: Specifies the default volume mode and access modes.
add: Add
volumeMode: Volume Mode
accessModes:
label: Access Modes
volumeSnapshotClass:
label: Volume Snapshot Class
tooltip: Sets the Volume Snapshot Class name to be used when taking snapshots of PVCs under this StorageClass.
cloneStrategy:
label: Clone Strategy
tooltip: Defines the clone strategy to use for volumes created with this StorageClass.
copy: Copy
snapshot: Snapshot
csiClone: CSI Clone
fileSystemOverhead:
label: File System Overhead
tooltip: Specifies the percentage of filesystem overhead to consider when calculating PVC size.
placeholder: e.g. 0.05 (up to 3 decimal places, between 0 and 1)
vlanConfig:
title: Network Configuration
@@ -1569,6 +1712,7 @@ advancedSettings:
'harv-backup-target': Custom backup target to store virtual machine backups.
'branding': Branding allows administrators to globally re-brand the UI by customizing the Harvester product name, logos, and color scheme.
'harv-csi-driver-config': Configure additional information for CSI drivers.
'harv-csi-online-expand-validation': Allow online volume expansion for specific CSI drivers.
'harv-containerd-registry': Containerd Registry Configuration to connect private registries.
'harv-log-level': Configure Harvester server log level. Defaults to Info.
'harv-server-version': Harvester server version.
@@ -1587,7 +1731,7 @@ advancedSettings:
'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down.
'harv-ssl-parameters': Custom SSL Parameters for TLS validation.
'harv-storage-network': 'Longhorn storage-network setting.'
'harv-support-bundle-namespaces': Specify resources in other namespaces to be collected by the support package.
'harv-support-bundle-namespaces': Select additional namespaces to include in the support bundle.
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage.
'harv-support-bundle-image': Support bundle image configuration. Find different versions in <a href="https://hub.docker.com/r/rancher/support-bundle-kit/tags" target="_blank">rancher/support-bundle-kit</a>.
'harv-release-download-url': This setting allows you to configure the <code>upgrade release download</code> URL address. Harvester will get the ISO URL and checksum value from the (<code>$URL</code>/<code>$VERSION</code>/version.yaml) file hosted by the configured URL.
@@ -1597,6 +1741,10 @@ advancedSettings:
'harv-kubeconfig-default-token-ttl-minutes': 'TTL (in minutes) applied on Harvester administration kubeconfig files. Default is 0, which means to never expire.'
'harv-longhorn-v2-data-engine-enabled': 'Enable the Longhorn V2 data engine. Default is false. <ul><li>Changing this setting will restart RKE2 on all nodes. This will not affect running VM workloads.</li><li>If you see "not enough hugepages-2Mi capacity" errors when enabling this setting, wait a minute for the error to clear. If the error remains, reboot the affected node.</li></ul>'
'harv-additional-guest-memory-overhead-ratio': 'The ratio for kubevirt to adjust the VM overhead memory. The value could be zero, empty value or floating number between 1.0 and 10.0, default to 1.5.'
'harv-upgrade-config': 'Configure image preloading and VM restore options for upgrades. See related fields in <a href="{url}" target="_blank" rel="noopener">settings/upgrade-config</a>'
'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.'
typeLabel:
kubevirt.io.virtualmachine: |-
@@ -1654,6 +1802,11 @@ typeLabel:
one { Virtual Machine Backup }
other { Virtual Machine Backups }
}
kubeovn.io.vpc: |-
{count, plural,
one { Virtual Private Cloud }
other { Virtual Private Clouds }
}
harvesterhci.io.cloudtemplate: |-
{count, plural,
one { Cloud Configuration Template }
+40 -4
View File
@@ -85,7 +85,7 @@ export default {
},
data() {
return { initSettings: [] };
return { initSettings: [], searchQuery: '' };
},
computed: {
@@ -116,7 +116,15 @@ export default {
return {
...s,
description: isHarvester ? `advancedSettings.descriptions.harv-${ s.id }` : `advancedSettings.descriptions.${ s.id }`,
customized: (!s.readOnly && s.data.value && s.data.value !== s.data.default) || s.data.hasCustomized
customized: (!s.readOnly && s.data.value && (
s.kind === 'json' ? (() => {
try {
return JSON.stringify(JSON.parse(s.data.value)) !== JSON.stringify(JSON.parse(s.data.default));
} catch {
return s.data.value !== s.data.default;
}
})() : s.data.value !== s.data.default
)) || s.data.hasCustomized
};
});
}
@@ -135,9 +143,19 @@ export default {
{{ t('harvester.setting.modifiedMessage') }}
</div>
</Banner>
<div class="fixed-header-actions harvester-settings-search">
<div class="search row">
<input
v-model="searchQuery"
type="search"
class="input-sm search-box"
:aria-label="t('sortableTable.searchLabel')"
:placeholder="t('sortableTable.search')"
>
</div>
</div>
<Tabbed
class="mt-30"
class="mt-15"
>
<Tab
name="advanced"
@@ -146,6 +164,7 @@ export default {
>
<Settings
:settings="settings"
:search-query="searchQuery"
category="advanced"
/>
</Tab>
@@ -156,6 +175,7 @@ export default {
>
<Settings
:settings="settings"
:search-query="searchQuery"
category="ui"
/>
</Tab>
@@ -167,4 +187,20 @@ export default {
.settings-banner {
margin-top: 0;
}
.harvester-settings-search {
padding: 0;
}
.search {
display: flex;
justify-content: flex-end;
}
.search-box {
height: 40px;
margin-left: 10px;
min-width: 180px;
}
</style>
+29 -1
View File
@@ -83,6 +83,34 @@ export default {
:rows="rows"
:schema="schema"
:headers="headers"
/>
>
<template #cell:name="{ row }">
<td>
<div>
<router-link
v-if="row?.detailLocation"
:to="row.detailLocation"
>
{{ row.nameDisplay }}
<i
v-if="row.isInternalStorageClass && typeof row.isInternalStorageClass === 'function' ? row.isInternalStorageClass() : false"
v-clean-tooltip="t('harvester.storage.internal.cannotDeleteOrDefaultTooltip')"
class="icon icon-info text-info"
style="margin-left: 0.4em;"
/>
</router-link>
<span v-else>
{{ row.nameDisplay }}
<i
v-if="row.isInternalStorageClass && typeof row.isInternalStorageClass === 'function' ? row.isInternalStorageClass() : false"
v-clean-tooltip="t('harvester.storage.internal.cannotDeleteOrDefaultTooltip')"
class="icon icon-info text-info"
style="margin-left: 0.4em;"
/>
</span>
</div>
</td>
</template>
</ResourceTable>
</div>
</template>
@@ -3,6 +3,8 @@ import ResourceTable from '@shell/components/ResourceTable';
import { Banner } from '@components/Banner';
import { defaultTableSortGenerationFn } from '@shell/components/ResourceTable.vue';
import FilterLabel from '../components/FilterLabel';
import { HCI as HCI_ANNOTATIONS } from '../config/labels-annotations';
import { isInternalStorageClass } from '../utils/storage-class';
export default {
name: 'ListHarvesterImage',
@@ -53,6 +55,13 @@ export default {
return base;
},
isInternalStorageClass(row) {
const name = row?.spec?.targetStorageClassName ||
row?.metadata?.annotations?.[HCI_ANNOTATIONS.STORAGE_CLASS];
return isInternalStorageClass(name);
},
}
};
</script>
@@ -90,6 +99,11 @@ export default {
v-if="row.isEncrypted"
class="icon icon-lock"
/>
<i
v-if="row.isImportedImage"
v-clean-tooltip="t('harvester.image.tooltip.imported')"
class="icon icon-info"
/>
</router-link>
<span v-else>
{{ row.nameDisplay }}
@@ -8,6 +8,7 @@ import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
import HarvesterVolumeState from '../formatters/HarvesterVolumeState';
import { allSettled } from '../utils/promise';
import { HCI, VOLUME_SNAPSHOT } from '../types';
import { INTERNAL_STORAGE_CLASS } from '../config/types';
const schema = {
id: HCI.VOLUME,
@@ -130,6 +131,10 @@ export default {
getVMName(row) {
return row.attachVM?.metadata?.name || '';
},
isInternalStorageClass(storageClassName) {
return this.$store.getters['type-map/labelFor'](INTERNAL_STORAGE_CLASS, storageClassName);
}
},
@@ -170,7 +175,7 @@ export default {
</router-link>
</div>
</template>
<template #col:name="{row}">
<template #col:name="{ row }">
<td>
<span>
<router-link
+285
View File
@@ -0,0 +1,285 @@
<script>
import Loading from '@shell/components/Loading';
import ResourceTable from '@shell/components/ResourceTable';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
import { NAME, AGE, NAMESPACE, STATE } from '@shell/config/table-headers';
import { PROVIDER, PROTOCOL, CIDR_BLOCK } from '@pkg/harvester/config/table-headers';
import { HCI } from '../types';
import { VPC } from '../config/query-params';
import { ADD_ONS } from '../config/harvester-map';
import { allHash } from '@shell/utils/promise';
import MessageLink from '@shell/components/MessageLink';
import Banner from '@components/Banner/Banner.vue';
export default {
name: 'HarvesterVPC',
components: {
ResourceTable,
Loading,
MessageLink,
Banner
},
inheritAttrs: false,
props: {
schema: {
type: Object,
required: true,
}
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = await allHash({ addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }) });
this.enabledKubeOvnAddon = hash.addons.find((addon) => addon.name === ADD_ONS.KUBEOVN_OPERATOR)?.spec?.enabled === true;
if (this.enabledKubeOvnAddon) {
try {
await allHash({
rows: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SUBNET }),
vpcs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
});
this.$store.dispatch('type-map/configureType', { match: HCI.SUBNET, isCreatable: this.enabledKubeOvnAddon });
} catch (e) {
// eslint-disable-next-line no-console
console.error('Error fetching VPC and Subnet data:', e);
}
}
},
data() {
return {
HCI,
hasBothSchema: false,
enabledKubeOvnAddon: false,
to: `${ HCI.ADD_ONS }/kube-system/${ ADD_ONS.KUBEOVN_OPERATOR }?mode=edit`
};
},
computed: {
headers() {
return [
STATE,
NAME,
NAMESPACE,
CIDR_BLOCK,
PROTOCOL,
PROVIDER,
AGE
];
},
rows() {
return this.$store.getters[`harvester/all`](HCI.SUBNET) || [];
},
vpcWithoutSubnets() {
const vpcs = this.$store.getters[`harvester/all`](HCI.VPC) || [];
const out = vpcs.map((v) => {
const hasChild = v.status?.subnets?.length > 0 || false;
return {
...v,
hasChild
};
});
return out;
},
isSubnetCreatable() {
return (this.subnetSchema?.collectionMethods || []).includes('POST');
},
rowsWithFakeVpcs() {
const fakeRows = this.vpcWithoutSubnets.map((vpc) => {
return {
groupByLabel: vpc.id,
isFake: true,
mainRowKey: vpc.id,
nameDisplay: vpc.id,
groupByVpc: vpc.id,
availableActions: []
};
});
return [...this.rows, ...fakeRows];
},
createVPCLocation() {
const location = {
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
params: {
product: HARVESTER_PRODUCT,
resource: HCI.VPC,
},
};
return location;
},
vpcSchema() {
return this.$store.getters[`harvester/schemaFor`](HCI.VPC);
},
subnetSchema() {
return this.$store.getters[`harvester/schemaFor`](HCI.SUBNET);
},
},
methods: {
groupLabel(group) {
return `${ this.t('harvester.vpc.label') }: ${ group.key }`;
},
slotName(vpc) {
return `main-row:${ vpc }`;
},
createSubnetLocation(group) {
const vpc = group.key;
const location = {
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
params: {
product: HARVESTER_PRODUCT,
resource: HCI.SUBNET,
},
};
location.query = { [VPC]: vpc };
return location;
},
showVpcAction(event, group) {
const vpc = group.key;
const resource = this.$store.getters[`harvester/byId`](HCI.VPC, vpc);
this.$store.commit(`action-menu/show`, {
resources: [resource],
elem: event.target
});
},
},
typeDisplay() {
return this.t('harvester.vpc.label');
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else-if="!enabledKubeOvnAddon">
<Banner color="warning">
<MessageLink
:to="to"
prefix-label="harvester.vpc.noAddonEnabled.prefix"
middle-label="harvester.vpc.noAddonEnabled.middle"
suffix-label="harvester.vpc.noAddonEnabled.suffix"
/>
</Banner>
</div>
<div v-else>
<Masthead
:schema="vpcSchema"
:type-display="t('harvester.vpc.label')"
:resource="HCI.VPC"
:create-location="createVPCLocation"
:create-button-label="t('harvester.clusterNetwork.create.button.label')"
/>
<ResourceTable
:rows="rowsWithFakeVpcs"
:headers="headers"
:schema="subnetSchema"
:groupable="true"
group-by="groupByVpc"
>
<template #header-middle>
<div />
</template>
<template #group-by="{group}">
<div class="group-bar">
<div class="group-tab">
<span>
{{ groupLabel(group) }}
</span>
</div>
<div class="right">
<router-link
v-if="isSubnetCreatable"
class="btn btn-sm role-secondary mr-5"
:to="createSubnetLocation(group)"
>
{{ t('harvester.vpc.createSubnet') }}
</router-link>
<button
type="button"
class="btn btn-sm role-multi-action actions mr-10"
@click="showVpcAction($event, group)"
>
<i class="icon icon-actions" />
</button>
</div>
</div>
</template>
<template
v-for="(vpc) in vpcWithoutSubnets"
:key="vpc.id"
v-slot:[slotName(vpc.id)]
>
<tr
v-show="!vpc.hasChild"
class="main-row"
>
<td
class="empty text-center"
colspan="12"
>
{{ t('harvester.vpc.noChild') }}
</td>
</tr>
</template>
</ResourceTable>
</div>
</template>
<style lang="scss" scoped>
.state {
display: flex;
justify-content: space-between;
.icon-warning {
margin-top: 2px;
}
}
.group-bar {
display: flex;
flex-direction: row;
justify-content: space-between;
.right {
margin-top: 5px;
margin-bottom: 3px;
}
.group-tab {
&, &::after {
height: 50px;
}
&::after {
right: -20px;
}
SPAN {
color: var(--body-text) !important;
}
}
}
</style>
@@ -22,8 +22,8 @@ export const VM_HEADERS = [
{
name: 'CPU',
label: 'CPU',
sort: ['spec.template.spec.domain.cpu.cores'],
value: 'spec.template.spec.domain.cpu.cores',
sort: ['displayCPU'],
value: 'displayCPU',
align: 'center',
dashIfEmpty: true,
},
@@ -2,12 +2,10 @@
import Loading from '@shell/components/Loading';
import ResourceTable from '@shell/components/ResourceTable';
import Masthead from '@shell/components/ResourceList/Masthead';
import { allHash } from '@shell/utils/promise';
import { STATE, AGE, NAME } from '@shell/config/table-headers';
import { mapPref, GROUP_RESOURCES } from '@shell/store/prefs';
import { NODE } from '@shell/config/types';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
import { CLUSTER_NETWORK } from '../config/query-params';
import { HCI } from '../types';
@@ -38,7 +36,6 @@ export default {
computed: {
groupPreference: mapPref(GROUP_RESOURCES),
headers() {
return [
STATE,
@@ -209,7 +206,6 @@ export default {
{{ groupLabel(group) }}
</span>
</div>
<div class="right">
<router-link
v-if="isClusterNetworkCreatable && group.key !== 'mgmt'"
+78 -12
View File
@@ -3,10 +3,9 @@ import jsyaml from 'js-yaml';
import isEqual from 'lodash/isEqual';
import isEmpty from 'lodash/isEmpty';
import difference from 'lodash/difference';
import { sortBy } from '@shell/utils/sort';
import { set } from '@shell/utils/object';
import { getVmCPUMemoryValues } from '../../utils/cpuMemory';
import { allHash } from '@shell/utils/promise';
import { randomStr } from '@shell/utils/string';
import { base64Decode } from '@shell/utils/crypto';
@@ -157,12 +156,16 @@ export default {
diskRows: [],
networkRows: [],
machineType: '',
machineTypes: [],
secretName: '',
secretRef: null,
showAdvanced: false,
deleteAgent: true,
memory: null,
cpu: '',
maxMemory: null,
maxCpu: '',
cpuMemoryHotplugEnabled: false,
reservedMemory: null,
accessCredentials: [],
efiEnabled: false,
@@ -295,6 +298,20 @@ export default {
async created() {
await this.$store.dispatch(`${ this.inStore }/findAll`, { type: SECRET });
if (this.value.vmMachineTypesFeatureEnabled) {
try {
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;
} catch (err) {
this.machineTypes = [''];
}
} else {
this.machineTypes = [''];
}
this.getInitConfig({ value: this.value, init: this.isCreate });
},
@@ -331,9 +348,13 @@ export default {
const maintenanceStrategy = vm.metadata.labels?.[HCI_ANNOTATIONS.VM_MAINTENANCE_MODE_STRATEGY] || 'Migrate';
const runStrategy = spec.runStrategy || 'RerunOnFailure';
const machineType = value.machineType;
const cpu = spec.template.spec.domain?.cpu?.cores;
const memory = spec.template.spec.domain.resources.limits.memory;
const machineType = spec.template.spec.domain?.machine?.type || this.machineTypes[0];
const {
cpu, memory, maxCpu, maxMemory, isHotplugEnabled
} = getVmCPUMemoryValues(vm);
const cpuMemoryHotplugEnabled = isHotplugEnabled;
const reservedMemory = vm.metadata?.annotations?.[HCI_ANNOTATIONS.VM_RESERVED_MEMORY];
const terminationGracePeriodSeconds = spec.template.spec?.terminationGracePeriodSeconds || this.defaultTerminationSetting;
@@ -393,6 +414,9 @@ export default {
this['cpu'] = cpu;
this['memory'] = memory;
this['maxCpu'] = maxCpu;
this['maxMemory'] = maxMemory;
this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
this['reservedMemory'] = reservedMemory;
this['machineType'] = machineType;
this['terminationGracePeriodSeconds'] = terminationGracePeriodSeconds;
@@ -617,20 +641,26 @@ export default {
this.spec.template.spec.domain.machine['type'] = this.machineType;
}
this.spec.template.spec.domain.cpu.cores = this.cpu;
this.spec.template.spec.domain.resources.limits.cpu = this.cpu ? this.cpu.toString() : this.cpu;
this.spec.template.spec.domain.resources.limits.memory = this.memory;
this.setCPUAndMemory();
// update terminationGracePeriodSeconds
this.spec.template.spec.terminationGracePeriodSeconds = this.terminationGracePeriodSeconds;
// parse reserved memory
const vm = this.resourceType === HCI.VM ? this.value : this.value.spec.vm;
// parse reserved memory
if (!this.reservedMemory) {
delete vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY];
} else {
vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY] = this.reservedMemory;
}
// add or remove cpu memory hotplug annotation
if (this.cpuMemoryHotplugEnabled) {
vm.metadata.annotations[HCI_ANNOTATIONS.VM_CPU_MEMORY_HOTPLUG] = this.cpuMemoryHotplugEnabled.toString();
} else {
delete vm.metadata.annotations[HCI_ANNOTATIONS.VM_CPU_MEMORY_HOTPLUG];
}
if (this.maintenanceStrategy === 'Migrate') {
delete vm.metadata.labels[HCI_ANNOTATIONS.VM_MAINTENANCE_MODE_STRATEGY];
} else {
@@ -638,6 +668,37 @@ export default {
}
},
setCPUAndMemory() {
if (this.cpuMemoryHotplugEnabled) {
// set CPU
this.spec.template.spec.domain.cpu.sockets = this.cpu;
this.spec.template.spec.domain.cpu.cores = 1;
// set max CPU
set(this.spec.template.spec, 'domain.cpu.maxSockets', this.maxCpu);
// domain.resources.limits.cpu and memory are defined by k8s which requires string values
// see https://kubernetes.io/docs/tasks/configure-pod-container/assign-cpu-resource/
set(this.spec.template.spec, 'domain.resources.limits.cpu', this.maxCpu?.toString());
// set memory
set(this.spec.template.spec, 'domain.memory.guest', this.memory);
// set max memory
set(this.spec.template.spec, 'domain.memory.maxGuest', this.maxMemory);
set(this.spec.template.spec, 'domain.resources.limits.memory', this.maxMemory);
} else {
this.spec.template.spec.domain.cpu.maxSockets = 1;
this.spec.template.spec.domain.cpu.sockets = 1;
this.spec.template.spec.domain.cpu.cores = this.cpu;
this.spec.template.spec.domain.resources.limits.cpu = this.cpu?.toString();
this.spec.template.spec.domain.resources.limits.memory = this.memory;
// clean below fields as we don't need them if not enable CPU and memory hotplug
if (this.spec?.template?.spec?.domain?.memory?.maxGuest) {
delete this.spec.template.spec.domain.memory.maxGuest;
}
}
},
parseDiskRows(disk) {
const disks = [];
const volumes = [];
@@ -936,9 +997,12 @@ export default {
this['sshKey'] = neu;
},
updateCpuMemory(cpu, memory) {
updateCpuMemory(cpu, memory, maxCpu = '', maxMemory = null, cpuMemoryHotplugEnabled = false) {
this['cpu'] = cpu;
this['memory'] = memory;
this['maxCpu'] = maxCpu;
this['maxMemory'] = maxMemory;
this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
},
parseDisk(R, index) {
@@ -1533,6 +1597,7 @@ export default {
if (val) {
this['sshKey'] = [];
this['userScript'] = undefined;
this['networkScript'] = undefined;
this['installAgent'] = false;
}
},
@@ -1590,8 +1655,9 @@ export default {
}
},
osType(neu) {
const out = this.getUserData({ installAgent: this.installAgent, osType: neu });
osType(neu, old) {
this.installAgent = old === 'windows' ? true : this.installAgent;
const out = old === 'windows' ? this.getInitUserData({ osType: neu }) : this.getUserData({ installAgent: this.installAgent, osType: neu });
this['userScript'] = out;
this.refreshYamlEditor();
+3 -1
View File
@@ -1,17 +1,19 @@
import SteveModel from '@shell/plugins/steve/steve-class';
import { VIEW_IN_API, DEV } from '@shell/store/prefs';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
import { HARVESTER_CRD_MAP } from '../config/harvester-map';
export default class HarvesterResource extends SteveModel {
get listLocation() {
const name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
const resource = HARVESTER_CRD_MAP[this.type] || this.type;
return this.$rootGetters['type-map/optionsFor'](this.type).customRoute || {
name,
params: {
product: this.$rootGetters['productId'],
cluster: this.$rootGetters['clusterId'],
resource: this.type,
resource
},
};
}
@@ -1,3 +1,4 @@
// This file is Cloud Configuration Templates page model
import { clone } from '@shell/utils/object';
import { HCI } from '../../types';
import HarvesterResource from '../harvester';
@@ -12,7 +12,7 @@ export default class HarvesterNetworkAttachmentDef extends NetworkAttachmentDef
params: {
product: HARVESTER_PRODUCT,
cluster: this.$rootGetters['clusterId'],
resource: this.type,
resource: HCI.NETWORK_ATTACHMENT,
},
};
}
@@ -9,6 +9,19 @@ export default class HciClusterflow extends HarvesterFlow {
return this.$rootGetters['harvester/all'](LOGGING.CLUSTER_OUTPUT) || [];
}
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.CLUSTER_FLOW;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get _detailLocation() {
const schema = this.$getters['schemaFor'](this.type);
@@ -21,6 +21,19 @@ export default class HciClusteroutput extends LogOutput {
};
}
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.CLUSTER_OUTPUT;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get detailLocation() {
const detailLocation = clone(this._detailLocation);
@@ -14,6 +14,19 @@ export default class HarvesterLogFlow extends Flow {
return this.$rootGetters['harvester/all'](LOGGING.CLUSTER_OUTPUT) || [];
}
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.FLOW;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get _detailLocation() {
const schema = this.$getters['schemaFor'](this.type);
@@ -22,6 +22,19 @@ export default class HarvesterLogOutput extends LogOutput {
};
}
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.OUTPUT;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get detailLocation() {
const detailLocation = clone(this._detailLocation);
@@ -19,6 +19,19 @@ export default class HciAlertmanagerConfig extends SteveModel {
};
}
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.ALERTMANAGERCONFIG;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get doneOverride() {
const detailLocation = clone(this._detailLocation);
+19
View File
@@ -5,6 +5,25 @@ import Secret from '@shell/models/secret';
import { NAMESPACE } from '@shell/config/types';
export default class HciSecret extends Secret {
// prevent harvester secret detail page be overridden.
// See isFullPageOverride in https://github.com/rancher/dashboard/blob/master/shell/components/ResourceDetail/index.vue
get fullDetailPageOverride() {
return false;
}
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.SECRET;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get _detailLocation() {
const schema = this.$getters['schemaFor'](this.type);
@@ -4,10 +4,24 @@ import { HCI } from '../../types';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
import { LONGHORN_DRIVER } from '@shell/config/types';
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
import { isInternalStorageClass } from '../../utils/storage-class';
export const LVM_DRIVER = 'lvm.driver.harvesterhci.io';
export default class HciStorageClass extends StorageClass {
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.STORAGE;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get detailLocation() {
const detailLocation = clone(this._detailLocation);
@@ -85,4 +99,23 @@ export default class HciStorageClass extends StorageClass {
get thirdPartyStorageFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
}
isInternalStorageClass() {
return isInternalStorageClass(this.metadata?.name);
}
get availableActions() {
let out = super.availableActions || [];
if (this.isInternalStorageClass()) {
out = out.filter((action) => {
return !['setDefault', 'setAsDefault', 'promptRemove'].includes(action.action);
});
}
if (out[0] && out[0].divider === true) {
out = out.slice(1);
}
return out;
}
}
@@ -109,15 +109,18 @@ export default class HciSetting extends HarvesterResource {
}
get parseValue() {
let parseDefaultValue = {};
try {
parseDefaultValue = JSON.parse(this.value);
if (this.value) {
return JSON.parse(this.value);
} else if (this.default) {
return JSON.parse(this.default);
}
} catch (err) {
parseDefaultValue = JSON.parse(this.default);
// eslint-disable-next-line no-console
console.error('Failed to parse setting value or default:', err);
}
return parseDefaultValue;
return {};
}
get isS3() {
@@ -153,4 +156,8 @@ export default class HciSetting extends HarvesterResource {
return out;
}
get disableResourceDetailDrawer() {
return true;
}
}
@@ -25,6 +25,10 @@ export default class HciVmBackup extends HarvesterResource {
return detailLocation;
}
get disableResourceDetailDrawerConfigTab() {
return true;
}
get doneOverride() {
const route = this.currentRoute();
const detailLocation = clone(this._detailLocation);
@@ -43,6 +43,11 @@ export default class HciVmImage extends HarvesterResource {
out = out.filter( (A) => !toFilter.includes(A.action));
// show `Clone` only when imageSource is `download`
if (this.imageSource !== 'download') {
out = out.filter(({ action }) => action !== 'goToClone');
}
const schema = this.$getters['schemaFor'](HCI.VM);
let canCreateVM = true;
@@ -52,40 +57,42 @@ export default class HciVmImage extends HarvesterResource {
const customActions = this.isReady ? [
{
action: 'createFromImage',
enabled: canCreateVM,
icon: 'icon icon-circle-plus',
label: this.t('harvester.action.createVM'),
action: 'createFromImage',
enabled: canCreateVM,
icon: 'icon icon-circle-plus',
label: this.t('harvester.action.createVM'),
},
{
action: 'encryptImage',
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted,
icon: 'icon icon-lock',
label: this.t('harvester.action.encryptImage'),
action: 'encryptImage',
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted,
icon: 'icon icon-lock',
label: this.t('harvester.action.encryptImage'),
},
{
action: 'decryptImage',
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted,
icon: 'icon icon-unlock',
label: this.t('harvester.action.decryptImage'),
action: 'decryptImage',
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted,
icon: 'icon icon-unlock',
label: this.t('harvester.action.decryptImage'),
},
{
action: 'imageDownload',
enabled: this.links?.download,
icon: 'icon icon-download',
label: this.t('asyncButton.download.action'),
action: 'imageDownload',
enabled: this.links?.download,
icon: 'icon icon-download',
label: this.t('asyncButton.download.action'),
}
] : [];
let filteredOut;
// imported image only allow download, download yaml and delete actions
if (this.isImportedImage) {
const custom = customActions.find((a) => a.action === 'imageDownload');
const filtered = out.filter(({ action }) => ['download', 'promptRemove'].includes(action));
if (customActions.length > 0) {
filteredOut = out;
} else {
// if the first item is a divider, remove it from the array
filteredOut = out[0]?.divider ? out.slice(1) : out;
return custom ? [custom, { divider: true }, ...filtered] : filtered;
}
// if the first item is a divider, remove it from the array
const filteredOut = customActions.length > 0 ? out : (out[0]?.divider ? out.slice(1) : out);
return [
...customActions,
...filteredOut
@@ -206,6 +213,10 @@ export default class HciVmImage extends HarvesterResource {
!!this.spec.securityParameters?.sourceImageNamespace;
}
get isImportedImage() {
return (this.metadata?.labels?.[HCI_ANNOTATIONS.IMPORTED_IMAGE]) === 'true';
}
get displayNameWithNamespace() {
return `${ this.metadata.namespace }/${ this.spec.displayName }`;
}
@@ -60,11 +60,11 @@ export default class HciVmTemplateVersion extends HarvesterResource {
applyDefaults() {
const spec = {
vm: {
metadata: { annotations: { [HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE]: '[]' } },
metadata: { annotations: { [HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE]: '[]' }, labels: {} },
spec: {
runStrategy: 'RerunOnFailure',
template: {
metadata: { annotations: {} },
metadata: { annotations: {}, labels: {} },
spec: {
domain: {
machine: { type: '' },
@@ -284,4 +284,10 @@ export default class HciVmTemplateVersion extends HarvesterResource {
get efiPersistentStateFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('efiPersistentState');
}
get systemAnnotations() {
const annotations = this.annotations || {};
return Object.keys(annotations).filter((key) => key.includes(HCI_ANNOTATIONS.TEMPLATE_VERSION_CUSTOM_NAME));
}
}

Some files were not shown because too many files have changed in this diff Show More