Compare commits

..

101 Commits

Author SHA1 Message Date
Andy Lee
5090d8ecad
ci: disable digest update (#966)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-01 16:54:30 +08:00
Andy Lee
b9334bafb7
feat: add overlay networks and underlay networks resources page (#938)
* feat: allow user to select kube-system ns

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

* feat: add NICs tab when creating overlay network

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

* refactor: only show NIC tab if ns is kube-system

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

* feat: add NAT & Internet tabs

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

* feat: add provider networks / vlan edit pages

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

* feat: add edit pages

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

* refactor: review comments

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

* refactor: copilot review

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-01 16:33:20 +08:00
Andy Lee
9a675da756
docs: add release block in README.md (#964)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-07-01 12:18:28 +08:00
renovate[bot]
8c0c36e022
deps: update patch dependencies (#945)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-01 11:22:07 +08:00
Siva Kanakala
140a85f6a8
fix: Fix VM Template UserData editor remaining editable in view-mode (#951)
Signed-off-by: Siva Kanakala <siva.kanakala@suse.com>
2026-07-01 11:21:26 +08:00
Volker Theile
e62ae7e1d8
fix: Fix the title of the CloudConfig DataTemplate dialog (#953)
Signed-off-by: Volker Theile <vtheile@suse.com>
2026-07-01 11:21:07 +08:00
Tim Serong
d03cff645b
feat: add Longhorn V2 hugepage settings (#942)
This exposes the longhorn-v2-data-engine-hugepage-enabled and
longhorn-v2-data-engine-memory-size settings in the Harvester GUI, on
Harvester v1.9.0 and later.

Related-to: https://github.com/harvester/harvester/issues/9390

Signed-off-by: Tim Serong <tserong@suse.com>
2026-06-25 14:39:23 +10:00
Volker Theile
34dfe4027e
fix: 'No Media' image selection removes 'harvesterhci.io/os' label from VM CR (#941)
When editing a Virtual Machine's volumes, if the user selects 'No media' for the Image, the 'harvesterhci.io/os' label is incorrectly removed from the VM's metadata.

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

Signed-off-by: Volker Theile <vtheile@suse.com>
2026-06-24 15:36:31 +02:00
Siva Kanakala
135d520b8d
fix: Fix CloudConfigTemplate UserData editor remaining editable in view-mode (#939)
Signed-off-by: Siva Kanakala <siva.kanakala@suse.com>
2026-06-24 11:33:19 +08:00
Andy Lee
62a19ee3fb
feat: add namespace filtering when updating namespaces (#940)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-06-23 21:44:39 +08:00
renovate[bot]
64f0f5fb87
deps: update patch dependencies (#932)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-22 14:36:19 +08:00
Andy Lee
93a692ff0c
feat: add NICs tab when creating overlay network (#931)
* feat: allow user to select kube-system ns

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

* feat: add NICs tab when creating overlay network

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

* refactor: only show NIC tab if ns is kube-system

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-06-22 14:32:26 +08:00
Andy Lee
5e3f12de35
feat: clear selected VM after doing bulk action (#929)
* feat: clear selected VM rows after click bulk action

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

* refactor: await altStopVM action

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

* refactor: multiple VM start actions

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

* refactor: modify altRestartVM()

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-06-17 13:55:01 +08:00
Po Han Huang
f115261889
feat: improve clone status (#928)
* feat: improve clone status

Signed-off-by: pohanhuang <pohan.huang@suse.com>

* fix: pr comments

Signed-off-by: pohanhuang <pohan.huang@suse.com>

---------

Signed-off-by: pohanhuang <pohan.huang@suse.com>
2026-06-17 11:10:43 +08:00
Andy Lee
5985913f5e
feat: add Host Networks tab list and edit pages (#920)
* feat: add Host Network tab

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

* feat: add Host Network edit page

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

* feat: add node selector tab

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

* refactor: copilot review

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

* fix: lint

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

* fix: copilot review

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

* refactor: add warning message if addon is not enabled or already hasone

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

* refactor: some wordings in en-us.yaml

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-06-16 10:57:15 +08:00
renovate[bot]
661ab995f6
deps: update patch dependencies (#921)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-15 14:01:50 +08:00
Andy Lee
d3f63df883
refactor: disable and hide filesystem warning banner in edit / view mode (#919)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-06-09 14:49:39 +08:00
Renuka Devi Rajendran
44ef9195eb
feat: add exclusive VLAN field to storage network settings (#914)
Signed-off-by: Renuka Devi Rajendran <renuka.rajendran@suse.com>
2026-06-08 08:44:24 -07:00
Andy Lee
b18aebbbd4
fix: snapshot/backup/volume attach VM name (#918)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-06-08 16:49:09 +08:00
renovate[bot]
d2db23d69a
deps: update minor dependencies to v7.29.7 (#915)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-08 15:40:18 +08:00
Andy Lee
55918232a6
feat: add volume expansion checkbox in storage class configuration (#911)
* feat: add support for volume expansion in storage configuration

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

* refactor: update volume expansion wording

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

* refactor: hide checkbox if feature flag is not enabled

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

* refactor: revert unneeded change

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-06-05 14:54:10 +08:00
Andy Lee
ce2adbdc3b
feat: support adding VM display name in creation page (#912)
* feat: add VM display name annotation support in edit and list views

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

* refactor: add display name checkbox and input field

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-06-05 12:11:07 +08:00
Jack Yu
ae65037083
feat: add disable resource pooling checkbox on pcidevice page (#902)
Signed-off-by: Jack Yu <jack.yu@suse.com>
2026-06-05 11:18:14 +08:00
Jack Yu
b4ea7c8f98
feat: remove old format pci device name checking (#889)
when implementing the first version pci device passthrough,
we didn't have our own device plugin, then resource name was changed
after upgrade.

Right now, we already have our own device plugin, which we can control
resource name by our ourselves. So, we can remove old mechanism.

Signed-off-by: Jack Yu <jack.yu@suse.com>
2026-06-05 11:18:04 +08:00
Andy Lee
836b04f222
feat: support filesystem volume in create VM page (#910)
* feat: add filesystem tab

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

* feat: add filesystem tab in create VM page

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

* refactor: update some wordings

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

* feat: add support for filesystem feature flag and enable filesystem tab

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

* refactor: remove unneeded wordings

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

* feat: add support for filesystem feature flag and update icon button positioning

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

* refactor: based on copilot review

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

* refactor: remove wrong feature flag

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

* fix: vm template

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-06-02 13:34:09 +08:00
Andy Lee
0908c7fc6b
Revert "feat: support filesystem disk for VM (#898)" (#909)
This reverts commit b34d618c7c0ab0447b26e2300c8141aefbb20427.

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-06-02 11:21:40 +08:00
Andy Lee
b34d618c7c
feat: support filesystem disk for VM (#898)
* feat: add filesystem tab

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

* feat: add filesystem tab in create VM page

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

* refactor: update some wordings

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

* feat: add support for filesystem feature flag and enable filesystem tab

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

* refactor: remove unneeded wordings

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

* feat: add support for filesystem feature flag and update icon button positioning

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

* refactor: based on copilot review

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

* refactor: tab name

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-06-02 10:33:04 +08:00
renovate[bot]
8945b9f158
deps: update dependency @types/node to v25.9.1 (#894)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-01 16:49:08 +08:00
renovate[bot]
c5af2b576b
deps: update dependency semver to v7.8.1 (#893)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-01 16:34:06 +08:00
Andy Lee
45872cef3b
feat: add autoattachPodInterface in edit VM mode if no network interface configured (#888)
* feat: add autoattachPodInterface in edit VM mode if no network interface configured

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

* refactor: remove unneeded code

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-31 15:51:51 +08:00
khushalchandak17
3e5ee422ce
fix(console): defer window.open to prevent dropdown staying open in Firefox (#890)
Signed-off-by: khushalchandak17 <khushal.chandak@suse.com>
2026-05-29 17:09:13 +08:00
Andy Lee
75202a9e55
chore: bump shell to 3.0.12-rc3 (#892)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-29 15:49:49 +08:00
Andy Lee
5f5ce291fb
ci: disable body-max-line-length eslint rule (#887)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-27 17:17:14 +08:00
Jack Yu
a27e38fc81
feat: display message from MIG status and change the state to match the case (#879)
Signed-off-by: Jack Yu <jack.yu@suse.com>
2026-05-26 13:44:27 +08:00
Andy Lee
89e1484884
feat: add natOutgoing option for custom subnet with corresponding tooltip (#878)
* feat: add external connectivity option for subnets with corresponding tooltip

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

* fix: remove gatewayType from subnet spec when not needed

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

* refactor: update external connectivity feature to use NAT outgoing and improve tooltip descriptions

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

* fix: update subnet creation to correctly handle natOutgoing and improve comment clarity

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

* refactor: improve tooltip clarity for external connectivity NAT option

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-26 12:13:59 +08:00
renovate[bot]
eacca055c7
deps: update minor dependencies (#884)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-25 18:02:44 +08:00
renovate[bot]
56c1738055
deps: update dependency roarr to v7.21.5 (#883)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-25 14:29:21 +08:00
renovate[bot]
8f4b335cae
deps: update dependency qs to v6.15.2 [security] (#880)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-25 14:24:22 +08:00
Andy Lee
91232beffc
fix: vGPU and USB enable/disable actions needs to be hidden for read only users (#877)
* fix: vGPU / USB enable/disable actions needs to be hidden for read only users

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

* style: add scoped styles for group actions in DeviceList component

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

* style: remove width property from group actions in DeviceList components

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

* fix: update logging type in init function and improve SideNav visibility handling

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

* fix: ensure canManageGroup only returns true for non-empty rows with updatable permissions

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-22 11:56:44 +08:00
Andy Lee
09e8946cc3
fix: missing VMIM in host detail VM tab (#876)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-18 14:55:02 +08:00
Volker Theile
e70c684382
feat: Prevent VM start after restore from backup or snapshot (#865)
* feat: Prevent VM start after restore from backup or snapshot

Related to: https://github.com/harvester/harvester/issues/10422
Related to: https://github.com/harvester/harvester/pull/10480

Signed-off-by: Volker Theile <vtheile@suse.com>

* feat: Prevent VM start after restore from backup or snapshot

Related to: https://github.com/harvester/harvester/issues/10422
Related to: https://github.com/harvester/harvester/pull/10480

Signed-off-by: Volker Theile <vtheile@suse.com>

---------

Signed-off-by: Volker Theile <vtheile@suse.com>
2026-05-18 08:10:36 +02:00
Andy Lee
1446aac168
refactor: enable add volume button regardless of rows length (#869)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-18 12:34:32 +08:00
renovate[bot]
8e65274b0c
deps: update dependency @types/node to v25.6.2 (#870)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-18 12:04:13 +08:00
renovate[bot]
e5432210b9
deps: update dependency semver to v7.8.0 (#871)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-18 12:00:49 +08:00
Andy Lee
af52df0ba0
fix: add alternative action for VM restart and soft reboot (#868)
* fix: add alternative action for VM restart and soft reboot

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

* refactor: fix alignment

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-15 12:03:20 +08:00
Andy Lee
e5a1929ac5
refactor: remove unused pluralization for persistent volume claim in localization file (#867)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-15 12:02:51 +08:00
Andy Lee
9de065a5c9
feat: allow migrate mutliple VMs functionality and localization (#863)
* feat: enhance VM migration functionality and localization

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

* refactor: add allVmsOnTargetNode method and update migration titles in localization

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

* feat: enhance migration error handling and update localization for migration dialog

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-13 15:59:13 +08:00
renovate[bot]
cd933bdbf8
deps: update dependency eslint-plugin-promise to v7.3.0 (#858)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-12 16:19:04 +08:00
Andy Lee
5fe642a42d
feat: add tooltip for route connectivity in network attachment definition (#864) 2026-05-12 16:14:11 +08:00
renovate[bot]
18c66083ab
deps: update dependency yaml to v2.8.4 (#857)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-12 15:59:13 +08:00
Andy Lee
d291a35754
feat: add PodSecurityStandard setting (#842)
* feat: add PodSecurity Standard setting

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

* refactor: unneeded change

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

* refactor: filter by isSystem namespace

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

* refactor: add fallback logic

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-07 13:15:47 +08:00
Andy Lee
5cc8b4c301
fix: some actions should be hidden for read-only user (#855)
* fix: some actions limited for virt-viewer

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

* refactor: based on schema collectionMethods

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

* fix: conditionally add migrate action to available actions

* fix: update collectionMethods check to use find for case-insensitive matching

* fix: update canEditClusterMembers method to use schema for collectionMethods

* fix: update canCreateImage check to use case-insensitive matching

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-07 10:21:26 +08:00
Andy Lee
032700293c
perf: improve vm list page performance (#835)
* fix: remove unneeded persistentvolumeclaim type label translation key

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

* refactor: improve lockIconTooltipMessage call twice

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

* refactor: avoid watch allVMs

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

* perf: improve the some functions with pre-created map

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

* perf: improve the vm list page

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

* refactor: AI comment

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

* refactor: based on feedback

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-04 16:33:59 +08:00
Jack Yu
8cb793e7ad
feat: add class type colume for usb device (#839)
Signed-off-by: Jack Yu <jack.yu@suse.com>
2026-05-04 15:17:01 +08:00
Andy Lee
2c45b71d1f
ci: remove brackets in PR Management (#852)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-04 14:30:02 +08:00
Andy Lee
5a301dcf55
fix: workflow not trigger (#849)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-04 13:50:24 +08:00
Andy Lee
1a92265d03
ci: replace pull_request_target with two-step workflows (#841)
* ci: update PR auto assign workflows

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

* ci: update backport label workflow

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

* ci: update backport PR via mergify workflow

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

* ci: update add PR label workflow

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

* refactor: file name

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

* refactor: limit auto-assign-check for target branches

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-03 23:11:55 +08:00
Andy Lee
8f65915bad
refactor: enable edit as yaml in VM image page (#843)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-03 23:05:36 +08:00
Andy Lee
67bb6dfbd5
docs: add AGENTS.md for AI agent guidance (#837)
* feat: add AGENNTS.md

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

* refactor: update based on copilot feedback

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

* refactor: update AGENTS.md

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

* refactor: update based on AI suggestion

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

* refactor: based on comments

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

* refactor: some files

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

* refactor: boundaries.md

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-04-29 16:05:51 +08:00
renovate[bot]
e74428d951
deps: update dependency @types/node to v25.6.0 (#830)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-27 11:44:54 +08:00
renovate[bot]
629f7df6b9
deps: update patch dependencies (#829)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-26 22:38:51 +08:00
Tim Liou
b7119d5c4c
chore: bump sha for upstream action pinning (#827)
Signed-off-by: Tim Liou <tim.liou@suse.com>
2026-04-24 16:36:43 +08:00
Andy Lee
6fdd1e3954
fix: change auth/V3user to auth/user (#824)
* fix: change auth/V3user to auth/user

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

* refactor: extract to utils/auth.js

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-04-22 15:39:24 +08:00
Andy Lee
7d0f33f31d
feat: show generic error message for API response 40x error (#816)
* feat: add generic error for API response 40X

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

* refactor: fallback error msg

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

* refactor: update error msg

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

* refactor: based on comment

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-04-21 17:55:30 +08:00
Andy Lee
9ce95daf76
ci: disable digest update in renovate.json (#823)
* ci: disable digest update in renovate.json

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

* refactor: keep but disable digest update auto merge

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

* refactor: add schedule for digest update

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-04-21 16:49:01 +08:00
Andy Lee
ce72232bc3
fix: failed build-extension-charts CI (#822)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-04-20 16:51:45 +08:00
Andy Lee
afc0e0f531
chore: bump @rancher/shell to 3.0.12-rc.1 (#810)
* chore: bump @rancher/shell to 3.0.12-rc.1

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

* ci: update node version to 24

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

* ci: update build catalog yaml

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

* fix: nav items order

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

* refactor: remove unneeded weightType

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-04-20 16:29:57 +08:00
renovate[bot]
e941cc9a90
deps: update dependency node-forge to v1.4.0 [security] (#780)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 12:14:47 +08:00
renovate[bot]
9961523d08
deps: update dependency follow-redirects to v1.16.0 [security] (#813)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 11:11:06 +08:00
Andy Lee
c4d1018388
fix: provisioner not update when deleting disk (#791)
* fix: provisioner not update when deleting disk

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

* refactor: update pkg/harvester/edit/harvesterhci.io.host/HarvesterDisk.vue

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: based on feedback

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-20 10:56:42 +08:00
Andy Lee
be64329776
feat: pop up dialog when enabling nvidia driver addon (#811)
* feat: add nvidia driver toolkit dialog

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

* refactor: add disable button guard

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

* refactor: based on feedback

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-04-17 11:00:10 +08:00
Andy Lee
35411ed87a
Revert "deps: update patch digest dependencies (#804)" (#808)
This reverts commit 15eb0f07f701a4da58d866984e07c2c3fd76eb3c.
2026-04-13 14:42:16 +08:00
renovate[bot]
15eb0f07f7
deps: update patch digest dependencies (#804)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 14:17:27 +08:00
Andy Lee
fb78f24fdd
chore: add v1.8.1 v1.9.0 feature flags (#792)
* chore: add v1.8.1 and v1.9.0 feature flags

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

* chore: bump to v1.9.0-dev

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-04-13 14:08:44 +08:00
renovate[bot]
81ad827829
deps: update fossas/fossa-action action to v1.9.0 (#794)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-07 16:10:40 +08:00
Andy Lee
6dd9b33336
ci: add add_minrelaseday to delay dep update (#798)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-04-07 15:22:11 +08:00
Tim Liou
1f9e9b336b
chore: bump version to 1.8.0-rc5 (#789)
Signed-off-by: Tim Liou <tim.liou@suse.com>
2026-04-01 09:07:56 +08:00
Andy Lee
c5b4f6cd1e
refactor: add banner in PCI Devices page (#785)
* refactor: add banner in PCI Devices page

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

* refactor: based on copilot review

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-31 17:45:06 +08:00
Andy Lee
4ce35ce075
fix: the rancher-eio/read-vault-secret sha (#786)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-31 14:02:41 +08:00
Andy Lee
27c26bd782
ci: remove unused EMBED_PKG variable (#783)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-31 13:48:43 +08:00
Tim Liou
9d698b1230
chore: bump version to 1.8.0-rc4 (#778)
Signed-off-by: Tim Liou <tim.liou@suse.com>
2026-03-27 16:38:29 +08:00
freeze
566e79eda5
feat: support advance option and creation from DataVolume (#776)
* feat: support advance option and creation from DataVolume

    - advance option would let user change the accessMode/volumeMode
    - creation from DataVolume could supports various scenario with
      3rd-party storage

Signed-off-by: Vicente Cheng <freeze.bilsted@gmail.com>

* feat: add data migration action on volume page

Signed-off-by: Vicente Cheng <freeze.bilsted@gmail.com>

* refactor: use show advanced options link instead of checkbox

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

* feat: add feature flag

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

* feat: add feature flag for dataMigration action

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

---------

Signed-off-by: Vicente Cheng <freeze.bilsted@gmail.com>
Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-26 17:29:14 +08:00
Andy Lee
42ddcfc1fe
chore: bump version to 1.8.0-rc3 (#774)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-25 17:27:39 +08:00
Andy Lee
ad3decf71f
fix: use default value got undefined value in payload (#772)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-25 17:08:32 +08:00
Andy Lee
8083a41df0
ci: remove the single quota for use commit hash (#767)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-25 14:08:59 +08:00
Andy Lee
46b860260a
ci: add CODEOWNERS file (#768)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-25 13:37:15 +08:00
freeze
62801b3b13
chore: pin GH Actions to commit sha (#765) 2026-03-25 10:12:25 +08:00
renovate[bot]
161e3bbd97
deps: update dependency yaml to v2.8.3 (#755)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-23 00:05:52 +08:00
Andy Lee
97e93dba0b
docs: update install instruction doc link (#753)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-20 16:36:09 +08:00
Andy Lee
9a8a709e56
refactor: change rwxNetwork setting to kind json (#751)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-20 13:28:30 +08:00
Andy Lee
d1949641a7
feat: introduce rwxNetwork setting (#746)
* feat: add rwxNetwork setting

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

* fix: network payload

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-19 15:55:55 +08:00
freeze
9c9f59c939
feat: add storage migration operation (#724)
- add storage migration
    - add cancel storage migration

Signed-off-by: Vicente Cheng <freeze.bilsted@gmail.com>
2026-03-19 11:45:54 +08:00
Andy Lee
ccc14c7fb9
chore: bump to v1.8.0-rc2 (#747)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-19 11:28:06 +08:00
Andy Lee
2ba471907e
feat: introduce instance-manager-resources setting (#744)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-18 17:16:12 +08:00
Andy Lee
5aea476f64
refactor: remove using resourceName to determine is vGPU device (#741) 2026-03-18 10:35:47 +08:00
Andy Lee
519c7d9f1f
fix: typo (#740)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-17 16:26:59 +08:00
Tim Liou
a9c392c13f
fix: reword the error message to focus on bootable volume (#736)
Signed-off-by: Tim Liou <tim.liou@suse.com>
2026-03-12 17:43:07 +08:00
Tim Liou
888ec7a50f
fix: container disks don't need volumeClaimTemplates but volumes (#735)
Signed-off-by: Tim Liou <tim.liou@suse.com>
2026-03-12 17:42:21 +08:00
Po Han Huang
a2486a7d38
feat: ensure the state is pending when perform cloning the efi (#730)
* feat: ensure the state is pending when perform cloning the efi

Signed-off-by: pohanhuang <pohan.huang@suse.com>

* feat: define harvesterhci.io/clone-backend-storage-status in labels-annotations.js

Signed-off-by: pohanhuang <pohan.huang@suse.com>

---------

Signed-off-by: pohanhuang <pohan.huang@suse.com>
2026-03-11 15:30:33 +08:00
Andy Lee
df3d249923
chore: bump to v1.8.0-rc1 (#731)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-11 15:29:27 +08:00
Andy Lee
23344e0c07
feat: add vGPU filter button and hide the enable/disable passthrough in PCIDevice page (#729)
* feat: add another filter button

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

* feat: add feature falg to hide vGPU enable/disable actions in PCIDevices page

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

* refactor: update with conditionally rendering

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-11 15:29:14 +08:00
82 changed files with 5295 additions and 2676 deletions

16
.github/renovate.json vendored
View File

@ -14,10 +14,17 @@
"semanticCommits": "enabled", "semanticCommits": "enabled",
"semanticCommitType": "deps", "semanticCommitType": "deps",
"prHourlyLimit": 12, "prHourlyLimit": 12,
"digest": {
"enabled": false
},
"timezone": "Asia/Taipei", "timezone": "Asia/Taipei",
"schedule": ["after 10am on sunday"], "schedule": ["after 10am on sunday"],
"postUpdateOptions": ["yarnDedupeFewer"], "postUpdateOptions": ["yarnDedupeFewer"],
"packageRules": [ "packageRules": [
{
"matchUpdateTypes": ["digest"],
"enabled": false
},
{ {
"matchUpdateTypes": ["major"], "matchUpdateTypes": ["major"],
"enabled": false "enabled": false
@ -39,11 +46,12 @@
"reviewers": ["a110605", "houhoucoop"] "reviewers": ["a110605", "houhoucoop"]
}, },
{ {
"matchUpdateTypes": ["patch", "digest"], "matchUpdateTypes": ["patch"],
"automerge": true, "automerge": false,
"minimumReleaseAge": "7 days", "minimumReleaseAge": "7 days",
"groupName": "patch digest dependencies", "groupName": "patch dependencies",
"labels": ["patch-update", "automerge"] "labels": ["patch-update"],
"reviewers": ["a110605", "houhoucoop"]
} }
] ]
} }

View File

@ -5,6 +5,19 @@ The Harvester UI Extension is a Rancher extension that provides the user interfa
> **Note:** > **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. > This extension is available starting from **Rancher 2.10.0**. Ensure your Rancher version is **2.10.0 or later** to access Harvester integration.
## Table of Contents
- [Installation](#installation)
- [Development Setup](#development-setup)
- [Commit Message Guidelines](#commit-message-guidelines)
- [Branch Structure](#branch-structure)
- [Testing Guidelines](#testing-guidelines)
- [Release](#release)
- [Contributing](#contributing)
- [License](#license)
## Installation ## Installation
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io). For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
@ -157,6 +170,13 @@ To test the standalone UI, configure Harvester to load the UI from an external s
2. Set **ui-source** to `External` 2. Set **ui-source** to `External`
3. Set **ui-index** to the desired URL 3. Set **ui-index** to the desired URL
## Release
The Harvester UI Extension follows the [Harvester](https://github.com/harvester/harvester) release cycle. After RC1 is cut for a new Harvester version, we usually create and work from the corresponding release branch (for example, `release-harvester-v1.8`). The remaining RC builds and the final official release are published from that branch.
After Harvester releases a new version, update the Harvester entry in rancher/ui-plugin-charts [manifest.json](https://github.com/rancher/ui-plugin-charts/blob/aafd215debbc6cb3100e7ba4b0a542c932397acd/manifest.json#L133-L151). This ensures air-gapped users can pull the new Harvester UI Extension image.
## Contributing ## Contributing
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. 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.

View File

@ -33,7 +33,7 @@ module.exports = {
'subject-full-stop': [2, 'never', '.'], 'subject-full-stop': [2, 'never', '.'],
'subject-max-length': [0, 'never'], 'subject-max-length': [0, 'never'],
'body-leading-blank': [2, 'always'], 'body-leading-blank': [2, 'always'],
'body-max-line-length': [2, 'always', 100], 'body-max-line-length': [0, 'always', 100],
'footer-leading-blank': [2, 'always'], 'footer-leading-blank': [2, 'always'],
'footer-max-line-length': [2, 'always', 100], 'footer-max-line-length': [2, 'always', 100],
}, },

View File

@ -1,13 +1,13 @@
{ {
"name": "harvester-ui-extension", "name": "harvester-ui-extension",
"version": "1.8.1-dev", "version": "1.9.0-dev",
"private": false, "private": false,
"engines": { "engines": {
"node": ">=24.0.0" "node": ">=24.0.0"
}, },
"dependencies": { "dependencies": {
"@babel/plugin-transform-class-static-block": "7.28.6", "@babel/plugin-transform-class-static-block": "7.29.7",
"@rancher/shell": "3.0.12-rc.1", "@rancher/shell": "3.0.12-rc.3",
"@vue-flow/background": "^1.3.0", "@vue-flow/background": "^1.3.0",
"@vue-flow/controls": "^1.1.1", "@vue-flow/controls": "^1.1.1",
"@vue-flow/core": "^1.33.5", "@vue-flow/core": "^1.33.5",
@ -21,7 +21,7 @@
"yaml": "^2.5.1" "yaml": "^2.5.1"
}, },
"resolutions": { "resolutions": {
"@types/node": "25.6.2", "@types/node": "25.9.4",
"cronstrue": "2.59.0", "cronstrue": "2.59.0",
"d3-color": "3.1.0", "d3-color": "3.1.0",
"ejs": "3.1.10", "ejs": "3.1.10",
@ -33,9 +33,9 @@
"merge": "2.1.1", "merge": "2.1.1",
"node-forge": "1.4.0", "node-forge": "1.4.0",
"nth-check": "2.1.1", "nth-check": "2.1.1",
"qs": "6.15.1", "qs": "6.15.2",
"roarr": "7.21.4", "roarr": "7.21.6",
"semver": "7.8.0", "semver": "7.8.5",
"@vue/cli-service/html-webpack-plugin": "^5.0.0" "@vue/cli-service/html-webpack-plugin": "^5.0.0"
}, },
"scripts": { "scripts": {

View File

@ -0,0 +1,186 @@
<script>
import { Banner } from '@components/Banner';
import MatchExpressions from '@shell/components/form/MatchExpressions';
import ResourceTable from '@shell/components/ResourceTable';
import { _EDIT } from '@shell/config/query-params';
import { convert, simplify, matching as selectorMatching } from '@shell/utils/selector';
import throttle from 'lodash/throttle';
import { NODE } from '@shell/config/types';
import { NAME, AGE } from '@shell/config/table-headers';
export default {
name: 'HarvesterNodeSelector',
components: {
Banner,
MatchExpressions,
ResourceTable,
},
props: {
mode: {
type: String,
default: _EDIT,
},
value: {
type: Object,
required: true,
},
},
async fetch() {
this.updateMatchingResources();
},
data() {
const inStore = this.$store.getters['currentProduct'].inStore;
return {
matchingResources: {
matched: 0,
matches: [],
none: true,
sample: null,
total: 0,
},
tableHeaders: [
NAME,
{
name: 'host-ip',
labelKey: 'tableHeaders.hostIp',
search: ['internalIp'],
value: 'internalIp',
sort: ['internalIp'],
align: 'center',
},
{
name: 'cpuManager',
labelKey: 'harvester.tableHeaders.cpuManager',
value: 'id',
formatter: 'HarvesterCPUPinning',
width: 150,
align: 'center',
},
{
name: 'diskState',
labelKey: 'tableHeaders.diskState',
value: 'diskState',
formatter: 'HarvesterDiskState',
width: 130,
},
AGE,
],
inStore,
};
},
watch: {
value: {
handler: 'updateMatchingResources',
deep: true,
},
allResourcesInScope() {
this.updateMatchingResources();
},
},
computed: {
schema() {
return this.$store.getters[`${ this.inStore }/schemaFor`](NODE);
},
selectorExpressions: {
get() {
return convert(
this.value.matchLabels || {},
this.value.matchExpressions || []
);
},
set(selectorExpressions) {
const { matchLabels, matchExpressions } = simplify(selectorExpressions);
this.value['matchLabels'] = matchLabels;
this.value['matchExpressions'] = matchExpressions;
this.updateMatchingResources();
},
},
allNodes() {
return this.$store.getters[`${ this.inStore }/all`](NODE) || [];
},
allResourcesInScope() {
return this.allNodes.length;
},
},
methods: {
updateMatchingResources: throttle(function() {
const expressions = this.selectorExpressions;
const allNodes = this.allNodes;
// Empty expressions with no key = no match
const hasValidExpression = expressions.length > 0 && expressions.every((e) => !!e.key);
if (!hasValidExpression) {
this.matchingResources = {
matched: 0,
matches: [],
none: true,
sample: null,
total: allNodes.length,
};
return;
}
const matches = selectorMatching(allNodes, expressions, 'metadata.labels');
this.matchingResources = {
matched: matches.length,
matches,
none: matches.length === 0,
sample: matches[0]?.nameDisplay || null,
total: allNodes.length,
};
}, 100, { trailing: true })
},
};
</script>
<template>
<div>
<div class="row">
<div class="col span-12">
<MatchExpressions
v-model:value="selectorExpressions"
:mode="mode"
:show-remove="false"
:type="'node'"
:target-resources="allResourcesInScope"
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<Banner :color="(matchingResources.none ? 'warning' : 'success')">
<span v-clean-html="t('generic.selectors.matchingResources.matchesSome', matchingResources)" />
</Banner>
</div>
</div>
<div class="row">
<div class="col span-12">
<ResourceTable
:rows="matchingResources.matches"
:headers="tableHeaders"
key-field="id"
:table-actions="false"
:row-actions="false"
:schema="schema"
:groupable="false"
:search="false"
/>
</div>
</div>
</div>
</template>

View File

@ -122,7 +122,7 @@ export default {
return this.$store.getters['currentCluster'].isLocal; return this.$store.getters['currentCluster'].isLocal;
}, },
canEditClusterMembers() { canEditClusterMembers() {
return this.normanClusterRTBSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post'); return this.schema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
}, },
}, },
}; };

View File

@ -58,11 +58,14 @@ export default {
const url = `https://${ host }${ prefix }/${ PRODUCT_NAME }/c/${ params.cluster }/console/${ uid }/${ type }`; const url = `https://${ host }${ prefix }/${ PRODUCT_NAME }/c/${ params.cluster }/console/${ uid }/${ type }`;
// Defer so v-select can finish closing the dropdown before the popup steals focus
this.$nextTick(() => {
window.open( window.open(
url, url,
'_blank', '_blank',
`toolbars=0,width=${ screen.width - 200 },height=${ screen.height - 200 },left=0,top=0,noreferrer` `toolbars=0,width=${ screen.width - 200 },height=${ screen.height - 200 },left=0,top=0,noreferrer`
); );
});
}, },
isEmpty(o) { isEmpty(o) {

View File

@ -0,0 +1,196 @@
<script>
import { RadioGroup } from '@components/Form/Radio';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { NAMESPACE } from '@shell/config/types';
import CreateEditView from '@shell/mixins/create-edit-view';
export default {
name: 'HarvesterClusterPodSecurityStandard',
components: {
RadioGroup,
LabeledSelect,
},
mixins: [CreateEditView],
props: {
value: {
type: Object,
default: () => ({}),
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE });
},
data() {
let enabled = false;
let whitelistedNamespaces = [];
let privilegedNamespaces = [];
let restrictedNamespaces = [];
try {
const parsed = JSON.parse(this.value.value || this.value.default || '{}');
enabled = !!parsed.enabled;
whitelistedNamespaces = parsed.whitelistedNamespacesList ? parsed.whitelistedNamespacesList.split(',') : [];
privilegedNamespaces = parsed.privilegedNamespacesList ? parsed.privilegedNamespacesList.split(',') : [];
restrictedNamespaces = parsed.restrictedNamespacesList ? parsed.restrictedNamespacesList.split(',') : [];
} catch (e) {
enabled = false;
whitelistedNamespaces = [];
privilegedNamespaces = [];
restrictedNamespaces = [];
}
return {
enabled,
whitelistedNamespaces,
privilegedNamespaces,
restrictedNamespaces,
};
},
computed: {
enabledOptions() {
return [
{ label: this.t('generic.enabled'), value: true },
{ label: this.t('generic.disabled'), value: false },
];
},
allNamespaces() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/all`](NAMESPACE).filter((ns) => !ns.isSystem).map((ns) => ns.id);
},
whitelistedOptions() {
const excluded = new Set([...this.privilegedNamespaces, ...this.restrictedNamespaces]);
return this.allNamespaces.filter((ns) => !excluded.has(ns));
},
privilegedOptions() {
const excluded = new Set([...this.whitelistedNamespaces, ...this.restrictedNamespaces]);
return this.allNamespaces.filter((ns) => !excluded.has(ns));
},
restrictedOptions() {
const excluded = new Set([...this.whitelistedNamespaces, ...this.privilegedNamespaces]);
return this.allNamespaces.filter((ns) => !excluded.has(ns));
},
},
methods: {
useDefault() {
try {
const parsed = JSON.parse(this.value.default || '{}');
this.enabled = !!parsed.enabled;
this.whitelistedNamespaces = parsed.whitelistedNamespacesList ? parsed.whitelistedNamespacesList.split(',') : [];
this.privilegedNamespaces = parsed.privilegedNamespacesList ? parsed.privilegedNamespacesList.split(',') : [];
this.restrictedNamespaces = parsed.restrictedNamespacesList ? parsed.restrictedNamespacesList.split(',') : [];
} catch (e) {
this.enabled = false;
this.whitelistedNamespaces = [];
this.privilegedNamespaces = [];
this.restrictedNamespaces = [];
}
this.save();
},
onUpdateEnabled() {
if (!this.enabled) {
this.whitelistedNamespaces = [];
this.privilegedNamespaces = [];
this.restrictedNamespaces = [];
}
this.save();
},
updateWhitelisted(selected) {
this.whitelistedNamespaces = selected;
this.save();
},
updatePrivileged(selected) {
this.privilegedNamespaces = selected;
this.save();
},
updateRestricted(selected) {
this.restrictedNamespaces = selected;
this.save();
},
save() {
this.value.value = JSON.stringify({
enabled: this.enabled,
whitelistedNamespacesList: this.whitelistedNamespaces.join(','),
privilegedNamespacesList: this.privilegedNamespaces.join(','),
restrictedNamespacesList: this.restrictedNamespaces.join(','),
});
},
},
};
</script>
<template>
<div>
<div class="row mb-20">
<div class="col span-6">
<RadioGroup
v-model:value="enabled"
name="enabled"
:options="enabledOptions"
@update:value="onUpdateEnabled"
/>
</div>
</div>
<template v-if="enabled">
<div class="row mb-20">
<div class="col span-12">
<LabeledSelect
v-model:value="whitelistedNamespaces"
:label="t('harvester.setting.clusterPodSecurityStandard.whitelistedNamespaces.label')"
:options="whitelistedOptions"
:multiple="true"
:mode="mode"
@update:value="updateWhitelisted"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-12">
<LabeledSelect
v-model:value="privilegedNamespaces"
:label="t('harvester.setting.clusterPodSecurityStandard.privilegedNamespaces.label')"
:options="privilegedOptions"
:multiple="true"
:mode="mode"
@update:value="updatePrivileged"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-12">
<LabeledSelect
v-model:value="restrictedNamespaces"
:label="t('harvester.setting.clusterPodSecurityStandard.restrictedNamespaces.label')"
:options="restrictedOptions"
:multiple="true"
:mode="mode"
@update:value="updateRestricted"
/>
</div>
</div>
</template>
</div>
</template>

View File

@ -64,6 +64,9 @@ export default {
try { try {
parsedDefaultValue = JSON.parse(this.value.value); parsedDefaultValue = JSON.parse(this.value.value);
if (typeof parsedDefaultValue.exclusiveVlan !== 'boolean') {
parsedDefaultValue.exclusiveVlan = false;
}
networkType = 'vlan' in parsedDefaultValue ? L2VLAN : UNTAGGED; // backend doesn't provide networkType, so we check if vlan is provided instead networkType = 'vlan' in parsedDefaultValue ? L2VLAN : UNTAGGED; // backend doesn't provide networkType, so we check if vlan is provided instead
openVlan = true; openVlan = true;
} catch (error) { } catch (error) {
@ -72,7 +75,8 @@ export default {
vlan: '', vlan: '',
clusterNetwork: '', clusterNetwork: '',
range: '', range: '',
exclude: [] exclude: [],
exclusiveVlan: false
}; };
} }
const exclude = parsedDefaultValue?.exclude?.toString().split(',') || []; const exclude = parsedDefaultValue?.exclude?.toString().split(',') || [];
@ -94,6 +98,10 @@ export default {
}, },
computed: { computed: {
showExclusiveVlan() {
return this.networkType === L2VLAN &&
Number(this.parsedDefaultValue.vlan) !== 1;
},
showVlan() { showVlan() {
return this.networkType === L2VLAN; return this.networkType === L2VLAN;
}, },
@ -132,6 +140,18 @@ export default {
}; };
}); });
}, },
exclusiveVlanOptions() {
return [
{
label: this.t('generic.enabled'),
value: true
},
{
label: this.t('generic.disabled'),
value: false
}
];
},
}, },
watch: { watch: {
@ -174,7 +194,8 @@ export default {
vlan: '', vlan: '',
clusterNetwork: '', clusterNetwork: '',
range: '', range: '',
exclude: [] exclude: [],
exclusiveVlan: false
}; };
}, },
@ -280,7 +301,15 @@ export default {
label-key="harvester.setting.storageNetwork.vlan" label-key="harvester.setting.storageNetwork.vlan"
@update:value="inputVlan" @update:value="inputVlan"
/> />
<LabeledSelect
v-if="showExclusiveVlan"
v-model:value="parsedDefaultValue.exclusiveVlan"
class="mb-20"
:options="exclusiveVlanOptions"
:mode="mode"
label-key="harvester.setting.storageNetwork.exclusiveVlan"
@update:value="update"
/>
<LabeledSelect <LabeledSelect
v-model:value="parsedDefaultValue.clusterNetwork" v-model:value="parsedDefaultValue.clusterNetwork"
label-key="harvester.setting.storageNetwork.clusterNetwork" label-key="harvester.setting.storageNetwork.clusterNetwork"

View File

@ -65,7 +65,15 @@ const FEATURE_FLAGS = {
'vGPUAsPCIDevice', 'vGPUAsPCIDevice',
'instanceManagerResourcesSetting', 'instanceManagerResourcesSetting',
'rwxNetworkSetting', 'rwxNetworkSetting',
'createPVCWithDataVolume' 'createPVCWithDataVolume',
'clusterPodSecurityStandardSetting',
],
'v1.8.1': [],
'v1.9.0': [
'supportFilesystem',
'disableResourcePooling',
'expandOnlineEncryptedVolume',
'longhornV2HugepageSettings'
], ],
}; };

View File

@ -54,6 +54,14 @@ import { registerAddonSideNav } from '../utils/dynamic-nav';
const TEMPLATE = HCI.VM_VERSION; const TEMPLATE = HCI.VM_VERSION;
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring'; const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
const LOGGING_GROUP = 'Monitoring & Logging::Logging'; const LOGGING_GROUP = 'Monitoring & Logging::Logging';
const OVERLAY_NETWORKS_GROUP = 'Overlay Networks';
const UNDERLAY_NETWORKS_GROUP = 'Underlay Networks';
const NAT_INTERNET_GROUP = `${ OVERLAY_NETWORKS_GROUP }::NAT & Internet`;
const GATEWAYS_GROUP = `${ NAT_INTERNET_GROUP }::Gateways`;
const EXTERNAL_IPS_GROUP = `${ NAT_INTERNET_GROUP }::External IPs`;
const RULES_GROUP = `${ NAT_INTERNET_GROUP }::Rules`;
const SOURCE_RULES_GROUP = `${ RULES_GROUP }::Source Rules`;
const DESTINATION_RULES_GROUP = `${ RULES_GROUP }::Destination Rules`;
export const PRODUCT_NAME = 'harvester'; export const PRODUCT_NAME = 'harvester';
@ -237,6 +245,7 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimport', labelKey: 'harvester.addons.vmImport.labels.vmimport',
group: 'vmimport', group: 'vmimport',
namespaced: true, namespaced: true,
ifHaveType: HCI.VMIMPORT,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT } params: { resource: HCI.VMIMPORT }
@ -266,6 +275,7 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare', labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare',
group: 'vmimport', group: 'vmimport',
namespaced: true, namespaced: true,
ifHaveType: HCI.VMIMPORT_SOURCE_V,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_V } params: { resource: HCI.VMIMPORT_SOURCE_V }
@ -295,6 +305,7 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack', labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack',
group: 'vmimport', group: 'vmimport',
namespaced: true, namespaced: true,
ifHaveType: HCI.VMIMPORT_SOURCE_O,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_O } params: { resource: HCI.VMIMPORT_SOURCE_O }
@ -323,6 +334,7 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA', labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA',
group: 'vmimport', group: 'vmimport',
namespaced: true, namespaced: true,
ifHaveType: HCI.VMIMPORT_SOURCE_OVA,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_OVA } params: { resource: HCI.VMIMPORT_SOURCE_OVA }
@ -484,6 +496,7 @@ export function init($plugin, store) {
}); });
virtualType({ virtualType({
ifHaveType: LOGGING.CLUSTER_FLOW,
labelKey: 'harvester.logging.clusterFlow.label', labelKey: 'harvester.logging.clusterFlow.label',
name: HCI.CLUSTER_FLOW, name: HCI.CLUSTER_FLOW,
namespaced: true, namespaced: true,
@ -507,6 +520,7 @@ export function init($plugin, store) {
}); });
virtualType({ virtualType({
ifHaveType: LOGGING.CLUSTER_OUTPUT,
labelKey: 'harvester.logging.clusterOutput.label', labelKey: 'harvester.logging.clusterOutput.label',
name: HCI.CLUSTER_OUTPUT, name: HCI.CLUSTER_OUTPUT,
namespaced: true, namespaced: true,
@ -530,6 +544,7 @@ export function init($plugin, store) {
}); });
virtualType({ virtualType({
ifHaveType: LOGGING.FLOW,
labelKey: 'harvester.logging.flow.label', labelKey: 'harvester.logging.flow.label',
name: HCI.FLOW, name: HCI.FLOW,
namespaced: true, namespaced: true,
@ -553,6 +568,7 @@ export function init($plugin, store) {
}); });
virtualType({ virtualType({
ifHaveType: LOGGING.OUTPUT,
labelKey: 'harvester.logging.output.label', labelKey: 'harvester.logging.output.label',
name: HCI.OUTPUT, name: HCI.OUTPUT,
namespaced: true, namespaced: true,
@ -573,14 +589,53 @@ export function init($plugin, store) {
[ [
HCI.CLUSTER_NETWORK, HCI.CLUSTER_NETWORK,
HCI.NETWORK_ATTACHMENT, HCI.NETWORK_ATTACHMENT,
HCI.VPC, HCI.HOST_NETWORK_CONFIG,
NETWORK_POLICY,
HCI.LB, HCI.LB,
HCI.IP_POOL, HCI.IP_POOL,
], ],
'networks' 'networks'
); );
basicType(
[HCI.VPC],
OVERLAY_NETWORKS_GROUP
);
basicType(
[NETWORK_POLICY],
OVERLAY_NETWORKS_GROUP
);
basicType(
[HCI.VPC_NAT_GATEWAY],
GATEWAYS_GROUP
);
basicType(
[HCI.IPTABLES_EIP],
EXTERNAL_IPS_GROUP
);
basicType(
[HCI.IPTABLES_SNAT_RULE],
SOURCE_RULES_GROUP
);
basicType(
[HCI.IPTABLES_DNAT_RULE],
DESTINATION_RULES_GROUP
);
basicType(
[HCI.PROVIDER_NETWORK],
UNDERLAY_NETWORKS_GROUP
);
basicType(
[HCI.VLAN],
UNDERLAY_NETWORKS_GROUP
);
basicType( basicType(
[ [
HCI.SCHEDULE_VM_BACKUP, HCI.SCHEDULE_VM_BACKUP,
@ -592,7 +647,11 @@ export function init($plugin, store) {
); );
weightGroup('networks', 494, true); weightGroup('networks', 494, true);
weightGroup('backupAndSnapshot', 493, true); weightGroup('Overlay Networks', 493, true);
weightGroup('NAT & Internet', 492, true);
weightGroup('Rules', 491, true);
weightGroup('Underlay Networks', 490, true);
weightGroup('backupAndSnapshot', 489, true);
basicType( basicType(
[ [
@ -671,7 +730,7 @@ export function init($plugin, store) {
name: HCI.CLUSTER_NETWORK, name: HCI.CLUSTER_NETWORK,
ifHaveType: HCI.CLUSTER_NETWORK, ifHaveType: HCI.CLUSTER_NETWORK,
namespaced: false, namespaced: false,
weight: 189, weight: 484,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.CLUSTER_NETWORK } params: { resource: HCI.CLUSTER_NETWORK }
@ -686,14 +745,14 @@ export function init($plugin, store) {
}, },
resource: NETWORK_ATTACHMENT, resource: NETWORK_ATTACHMENT,
resourceDetail: HCI.NETWORK_ATTACHMENT, resourceDetail: HCI.NETWORK_ATTACHMENT,
resourceEdit: HCI.NETWORK_ATTACHMENT resourceEdit: HCI.NETWORK_ATTACHMENT,
}); });
virtualType({ virtualType({
labelKey: 'harvester.network.label', labelKey: 'harvester.network.label',
name: HCI.NETWORK_ATTACHMENT, name: HCI.NETWORK_ATTACHMENT,
namespaced: true, namespaced: true,
weight: 188, weight: 485,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.NETWORK_ATTACHMENT } params: { resource: HCI.NETWORK_ATTACHMENT }
@ -707,7 +766,7 @@ export function init($plugin, store) {
labelKey: 'harvester.vpc.label', labelKey: 'harvester.vpc.label',
name: HCI.VPC, name: HCI.VPC,
namespaced: true, namespaced: true,
weight: 187, weight: 195,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VPC } params: { resource: HCI.VPC }
@ -716,13 +775,73 @@ export function init($plugin, store) {
ifHaveType: HCI.VPC, ifHaveType: HCI.VPC,
}); });
configureType(HCI.VPC_NAT_GATEWAY, { hiddenNamespaceGroupButton: true, canYaml: false });
virtualType({
labelKey: 'harvester.natGateway.label',
name: HCI.VPC_NAT_GATEWAY,
namespaced: false,
weight: 193,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VPC_NAT_GATEWAY }
},
exact: false,
ifHaveType: HCI.VPC_NAT_GATEWAY,
});
configureType(HCI.IPTABLES_EIP, { hiddenNamespaceGroupButton: true, canYaml: false });
virtualType({
labelKey: 'harvester.externalIP.label',
name: HCI.IPTABLES_EIP,
namespaced: false,
weight: 192,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.IPTABLES_EIP }
},
exact: false,
ifHaveType: HCI.IPTABLES_EIP,
});
configureType(HCI.IPTABLES_SNAT_RULE, { hiddenNamespaceGroupButton: true, canYaml: false });
virtualType({
labelKey: 'harvester.snat.label',
name: HCI.IPTABLES_SNAT_RULE,
namespaced: false,
weight: 191,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.IPTABLES_SNAT_RULE }
},
exact: false,
ifHaveType: HCI.IPTABLES_SNAT_RULE,
});
configureType(HCI.IPTABLES_DNAT_RULE, { hiddenNamespaceGroupButton: true, canYaml: false });
virtualType({
labelKey: 'harvester.dnat.label',
name: HCI.IPTABLES_DNAT_RULE,
namespaced: false,
weight: 190,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.IPTABLES_DNAT_RULE }
},
exact: false,
ifHaveType: HCI.IPTABLES_DNAT_RULE,
});
configureType(NETWORK_POLICY, { hiddenNamespaceGroupButton: true, canYaml: false }); configureType(NETWORK_POLICY, { hiddenNamespaceGroupButton: true, canYaml: false });
virtualType({ virtualType({
labelKey: 'harvester.networkPolicy.label', labelKey: 'harvester.networkPolicy.label',
name: NETWORK_POLICY, name: NETWORK_POLICY,
namespaced: true, namespaced: true,
weight: 186, weight: 194,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: NETWORK_POLICY } params: { resource: NETWORK_POLICY }
@ -731,6 +850,53 @@ export function init($plugin, store) {
ifHaveType: NETWORK_POLICY, ifHaveType: NETWORK_POLICY,
}); });
configureType(HCI.PROVIDER_NETWORK, { hiddenNamespaceGroupButton: true, canYaml: false });
virtualType({
labelKey: 'harvester.providerNetwork.label',
name: HCI.PROVIDER_NETWORK,
namespaced: false,
weight: 189,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.PROVIDER_NETWORK }
},
exact: false,
ifHaveType: HCI.PROVIDER_NETWORK,
});
configureType(HCI.VLAN, { hiddenNamespaceGroupButton: true, canYaml: false });
headers(HCI.VLAN, [
STATE,
NAME_COL,
{
name: 'id',
label: 'ID',
value: 'spec.id',
sort: 'spec.id'
},
{
name: 'provider',
labelKey: 'harvester.subnet.provider.label',
value: 'spec.provider',
sort: 'spec.provider'
}
]);
virtualType({
labelKey: 'harvester.vlanNetwork.label',
name: HCI.VLAN,
namespaced: false,
weight: 188,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VLAN }
},
exact: false,
ifHaveType: HCI.VLAN,
});
configureType(HCI.SNAPSHOT, { configureType(HCI.SNAPSHOT, {
isCreatable: false, isCreatable: false,
location: { location: {
@ -1085,7 +1251,7 @@ export function init($plugin, store) {
labelKey: 'harvester.loadBalancer.label', labelKey: 'harvester.loadBalancer.label',
name: HCI.LB, name: HCI.LB,
namespaced: true, namespaced: true,
weight: 185, weight: 483,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.LB } params: { resource: HCI.LB }
@ -1124,7 +1290,7 @@ export function init($plugin, store) {
labelKey: 'harvester.ipPool.label', labelKey: 'harvester.ipPool.label',
name: HCI.IP_POOL, name: HCI.IP_POOL,
namespaced: false, namespaced: false,
weight: 184, weight: 482,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.IP_POOL } params: { resource: HCI.IP_POOL }
@ -1133,4 +1299,24 @@ export function init($plugin, store) {
ifHaveType: HCI.IP_POOL, ifHaveType: HCI.IP_POOL,
}); });
headers(HCI.IP_POOL, IP_POOL_HEADERS); headers(HCI.IP_POOL, IP_POOL_HEADERS);
configureType(HCI.HOST_NETWORK_CONFIG, {
location: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.HOST_NETWORK_CONFIG }
},
canYaml: false,
});
virtualType({
labelKey: 'harvester.hostNetworkConfig.label',
name: HCI.HOST_NETWORK_CONFIG,
namespaced: false,
weight: 481,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.HOST_NETWORK_CONFIG }
},
exact: false,
ifHaveType: HCI.HOST_NETWORK_CONFIG,
});
} }

View File

@ -88,6 +88,8 @@ export const CSI_SECRETS = {
CSI_NODE_PUBLISH_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-publish-secret-namespace', CSI_NODE_PUBLISH_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-publish-secret-namespace',
CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name', 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', CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace',
CSI_NODE_EXPAND_SECRET_NAME: 'csi.storage.k8s.io/node-expand-secret-name',
CSI_NODE_EXPAND_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-expand-secret-namespace'
}; };
// Some harvester CRD type is not equal to model file name, define the mapping here // Some harvester CRD type is not equal to model file name, define the mapping here

View File

@ -19,6 +19,7 @@ export const HCI = {
NETWORK_TYPE: 'network.harvesterhci.io/type', NETWORK_TYPE: 'network.harvesterhci.io/type',
VM_NAME: 'harvesterhci.io/vmName', VM_NAME: 'harvesterhci.io/vmName',
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix', VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
VM_DISPLAY_NAME: 'harvesterhci.io/vmDisplayName',
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory', VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status', MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name', HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
@ -81,4 +82,5 @@ export const HCI = {
MAC_ADDRESS: 'harvesterhci.io/mac-address', MAC_ADDRESS: 'harvesterhci.io/mac-address',
NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map', NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map',
CDI_POPULATOR_KIND: 'cdi.kubevirt.io/storage.populator.kind', CDI_POPULATOR_KIND: 'cdi.kubevirt.io/storage.populator.kind',
CNI_NETWORKS: 'k8s.v1.cni.cncf.io/networks',
}; };

View File

@ -35,13 +35,16 @@ export const HCI_SETTING = {
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs', AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes', KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled', LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
LONGHORN_V2_DATA_ENGINE_HUGEPAGE_ENABLED: 'longhorn-v2-data-engine-hugepage-enabled',
LONGHORN_V2_DATA_ENGINE_MEMORY_SIZE: 'longhorn-v2-data-engine-memory-size',
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio', ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
UPGRADE_CONFIG: 'upgrade-config', UPGRADE_CONFIG: 'upgrade-config',
VM_MIGRATION_NETWORK: 'vm-migration-network', VM_MIGRATION_NETWORK: 'vm-migration-network',
RANCHER_CLUSTER: 'rancher-cluster', RANCHER_CLUSTER: 'rancher-cluster',
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio', MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
KUBEVIRT_MIGRATION: 'kubevirt-migration', KUBEVIRT_MIGRATION: 'kubevirt-migration',
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources' INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources',
CLUSTER_POD_SECURITY_STANDARD: 'cluster-pod-security-standard'
}; };
export const HCI_ALLOWED_SETTINGS = { export const HCI_ALLOWED_SETTINGS = {
@ -111,6 +114,14 @@ export const HCI_ALLOWED_SETTINGS = {
experimental: true, experimental: true,
featureFlag: 'longhornV2LVMSupport' featureFlag: 'longhornV2LVMSupport'
}, },
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_HUGEPAGE_ENABLED]: {
kind: 'boolean',
featureFlag: 'longhornV2HugepageSettings'
},
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_MEMORY_SIZE]: {
kind: 'number',
featureFlag: 'longhornV2HugepageSettings'
},
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' }, [HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
[HCI_SETTING.UPGRADE_CONFIG]: { [HCI_SETTING.UPGRADE_CONFIG]: {
kind: 'json', kind: 'json',
@ -130,6 +141,9 @@ export const HCI_ALLOWED_SETTINGS = {
}, },
[HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: { [HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: {
kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting' kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting'
},
[HCI_SETTING.CLUSTER_POD_SECURITY_STANDARD]: {
kind: 'json', from: 'import', canReset: true, featureFlag: 'clusterPodSecurityStandardSetting'
} }
}; };

View File

@ -46,3 +46,9 @@ export const CDI_POPULATOR_KIND = {
VOLUME_IMPORT_SOURCE: 'VolumeImportSource', VOLUME_IMPORT_SOURCE: 'VolumeImportSource',
VOLUME_CLONE_SOURCE: 'VolumeCloneSource', VOLUME_CLONE_SOURCE: 'VolumeCloneSource',
}; };
export const FILESYSTEM_SOURCE_TYPE = {
CONFIGMAP: 'configmap',
SECRET: 'secret',
SERVICEACCOUNT: 'serviceaccount',
};

View File

@ -27,15 +27,11 @@ export default {
await allHash({ await allHash({
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }), vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }), vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }),
allClusterNetwork: this.$store.dispatch('harvester/findAll', { type: HCI.CLUSTER_NETWORK }), vmims: this.$store.dispatch('harvester/findAll', { type: HCI.VMIM }),
}); });
}, },
computed: { computed: {
allClusterNetwork() {
return this.$store.getters['harvester/all'](HCI.CLUSTER_NETWORK);
},
rows() { rows() {
const vms = this.$store.getters['harvester/all'](HCI.VM); const vms = this.$store.getters['harvester/all'](HCI.VM);
@ -108,7 +104,6 @@ export default {
<HarvesterVmState <HarvesterVmState
class="vmstate" class="vmstate"
:row="scope.row" :row="scope.row"
:all-cluster-network="allClusterNetwork"
/> />
</div> </div>
</template> </template>

View File

@ -98,9 +98,9 @@ export default {
methods: { methods: {
escapeHtml, escapeHtml,
close() { close(data) {
this.errors = []; this.errors = [];
this.$emit('close'); this.$emit('close', data);
}, },
async apply(buttonDone) { async apply(buttonDone) {
@ -109,7 +109,7 @@ export default {
await resource.doActionGrowl(this.modalData.action, {}); await resource.doActionGrowl(this.modalData.action, {});
} }
buttonDone(true); buttonDone(true);
this.close(); this.close({ performCallback: true, clearTableSelection: true });
} catch (e) { } catch (e) {
this.errors = exceptionToErrorsArray(e); this.errors = exceptionToErrorsArray(e);
buttonDone(false); buttonDone(false);

View File

@ -1,6 +1,8 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { Banner } from '@components/Banner';
import { Card } from '@components/Card'; import { Card } from '@components/Card';
import { Checkbox } from '@components/Form/Checkbox';
import AsyncButton from '@shell/components/AsyncButton'; import AsyncButton from '@shell/components/AsyncButton';
import { escapeHtml } from '@shell/utils/string'; import { escapeHtml } from '@shell/utils/string';
import { HCI } from '../types'; import { HCI } from '../types';
@ -13,7 +15,9 @@ export default {
components: { components: {
AsyncButton, AsyncButton,
Banner,
Card, Card,
Checkbox,
}, },
props: { props: {
@ -24,10 +28,16 @@ export default {
}, },
data() { data() {
return {}; return { disableResourcePooling: false };
}, },
computed: { ...mapGetters({ t: 'i18n/t' }) }, computed: {
...mapGetters({ t: 'i18n/t' }),
disableResourcePoolingEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('disableResourcePooling');
},
},
methods: { methods: {
close() { close() {
@ -54,7 +64,8 @@ export default {
spec: { spec: {
address: actionResource.status.address, address: actionResource.status.address,
nodeName: actionResource.status.nodeName, nodeName: actionResource.status.nodeName,
userName userName,
disableResourcePooling: this.disableResourcePooling,
} }
} ); } );
@ -85,7 +96,19 @@ export default {
</template> </template>
<template #body> <template #body>
<p class="mb-20">
{{ t('harvester.pci.enablePassthroughWarning') }} {{ t('harvester.pci.enablePassthroughWarning') }}
</p>
<template v-if="disableResourcePoolingEnabled">
<Checkbox
v-model:value="disableResourcePooling"
label-key="harvester.pci.disableResourcePooling"
/>
<Banner
color="info"
:label="t('harvester.pci.disableResourcePoolingDescription')"
/>
</template>
</template> </template>
<template #actions> <template #actions>

View File

@ -0,0 +1,154 @@
<script>
import merge from 'lodash/merge';
import jsyaml from 'js-yaml';
import { mapGetters } from 'vuex';
import { Card } from '@components/Card';
import { LabeledInput } from '@components/Form/LabeledInput';
import AsyncButton from '@shell/components/AsyncButton';
import { escapeHtml } from '@shell/utils/string';
const DEFAULT_VALUE = { image: { repository: 'rancher/harvester-nvidia-driver-toolkit' } };
export default {
name: 'HarvesterEnableNvidiaDriverToolkit',
emits: ['close'],
components: {
AsyncButton,
Card,
LabeledInput,
},
props: {
resources: {
type: Array,
required: true
}
},
data() {
const addon = this.resources[0];
let valuesContentJson;
try {
valuesContentJson = merge({}, DEFAULT_VALUE, jsyaml.load(addon.spec.valuesContent));
} catch (e) {
valuesContentJson = { ...DEFAULT_VALUE };
}
return { valuesContentJson };
},
computed: {
...mapGetters({ t: 'i18n/t' }),
buttonDisabled() {
const { image, driverLocation } = this.valuesContentJson;
return !(image?.repository || '').trim() || !(image?.tag || '').trim() || !(driverLocation || '').trim();
}
},
methods: {
close() {
this.$emit('close');
},
async enable(buttonCb) {
const addon = this.resources[0];
try {
addon.spec.valuesContent = jsyaml.dump(this.valuesContentJson);
addon.spec.enabled = true;
await addon.save();
buttonCb(true);
this.close();
} catch (err) {
addon.spec.enabled = false;
this.$store.dispatch('growl/fromError', {
title: this.t('generic.notification.title.error', { name: escapeHtml(addon.metadata.name) }),
err,
}, { root: true });
buttonCb(false);
}
}
}
};
</script>
<template>
<Card :show-highlight-border="false">
<template #title>
<h4
v-clean-html="t('harvester.addons.nvidiaDriverToolkit.enable.title')"
class="text-default-text"
/>
</template>
<template #body>
<div class="body">
<div class="row mb-15">
<div class="col span-6">
<LabeledInput
v-model:value="valuesContentJson.image.repository"
:required="true"
:label="t('harvester.addons.nvidiaDriverToolkit.image.repository')"
/>
</div>
<div class="col span-6">
<LabeledInput
v-model:value="valuesContentJson.image.tag"
:required="true"
:label="t('harvester.addons.nvidiaDriverToolkit.image.tag')"
/>
</div>
</div>
<div class="row mb-15">
<div class="col span-12">
<LabeledInput
v-model:value="valuesContentJson.driverLocation"
:required="true"
:label="t('harvester.addons.nvidiaDriverToolkit.driver.location')"
/>
</div>
</div>
</div>
</template>
<template #actions>
<div class="buttons actions">
<button
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="enable"
:disabled="buttonDisabled"
@click="enable"
/>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.body {
display: flex;
flex-direction: column;
min-width: 400px;
}
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>

View File

@ -1,15 +1,12 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { NODE } from '@shell/config/types'; import { NODE } from '@shell/config/types';
import { exceptionToErrorsArray } from '@shell/utils/error'; import { exceptionToErrorsArray } from '@shell/utils/error';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { Card } from '@components/Card'; import { Card } from '@components/Card';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton'; import AsyncButton from '@shell/components/AsyncButton';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI } from '../types';
export default { export default {
emits: ['close'], emits: ['close'],
@ -62,28 +59,46 @@ export default {
return this.resources[0]; return this.resources[0];
}, },
vmi() { anyCpuPinning() {
const inStore = this.$store.getters['currentProduct'].inStore; return this.resources.some((r) => r.isCpuPinning);
const vmiResources = this.$store.getters[`${ inStore }/all`](HCI.VMI); },
const resource = vmiResources.find((VMI) => VMI.id === this.actionResource?.id) || null;
return resource; vmsByNode() {
const groups = {};
for (const r of this.resources) {
const node = r.nodeName || '';
const name = r.nameDisplay || r.name || r.id;
if (!groups[node]) {
groups[node] = [];
}
groups[node].push(name);
}
return Object.entries(groups).map(([node, vms]) => ({ node, vms })).sort((a, b) => a.node.localeCompare(b.node));
}, },
cpuPinningAlertMessage() { cpuPinningAlertMessage() {
return this.t('harvester.virtualMachine.cpuPinning.migrationMessage'); return this.t('harvester.virtualMachine.cpuPinning.migrationMessage');
}, },
allVmsOnTargetNode() {
if (!this.nodeName) {
return false;
}
return this.resources.every((r) => r.nodeName === this.nodeName);
},
nodeNameList() { nodeNameList() {
const nodes = this.$store.getters['harvester/all'](NODE); const nodes = this.$store.getters['harvester/all'](NODE);
return nodes.filter((n) => { return nodes.filter((n) => {
const isNotSelfNode = !!this.availableNodes.includes(n.id);
const isNotWitnessNode = n.isEtcd !== 'true'; // do not allow to migrate to self node and witness node const isNotWitnessNode = n.isEtcd !== 'true'; // do not allow to migrate to self node and witness node
const isCpuPinning = this.actionResource?.isCpuPinning; const matchingCpuManagerConfig = !this.anyCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
const matchingCpuManagerConfig = !isCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
return isNotSelfNode && isNotWitnessNode && matchingCpuManagerConfig; return isNotWitnessNode && matchingCpuManagerConfig;
}).map((n) => { }).map((n) => {
let label = n?.metadata?.name; let label = n?.metadata?.name;
const value = n?.metadata?.name; const value = n?.metadata?.name;
@ -102,10 +117,10 @@ export default {
}, },
methods: { methods: {
close() { close(data) {
this.nodeName = ''; this.nodeName = '';
this.errors = []; this.errors = [];
this.$emit('close'); this.$emit('close', data);
}, },
async apply(buttonDone) { async apply(buttonDone) {
@ -126,10 +141,32 @@ export default {
} }
try { try {
await this.actionResource.doAction('migrate', { nodeName: this.nodeName }, {}, false); // Filter out VMs already running on the selected node
const toMigrate = this.resources.filter((r) => r.nodeName !== this.nodeName);
// await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
// We want to show all migration errors if there are multiple VMs, so we use allSettled here and handle the results accordingly.
const results = await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
const failedMigrations = results
.map((result, index) => ({ resource: toMigrate[index], result }))
.filter(({ result }) => result.status === 'rejected');
if (failedMigrations.length) {
this['errors'] = failedMigrations.flatMap(({ resource, result }) => {
const vmName = resource?.nameDisplay || resource?.name || resource?.metadata?.name || this.$store.getters['i18n/t']('generic.unknown');
const error = result.reason?.data || result.reason;
const messages = exceptionToErrorsArray(error);
return messages.map((message) => `${ vmName }: ${ message }`);
});
buttonDone(false);
return;
}
buttonDone(true); buttonDone(true);
this.close(); this.close({ performCallback: true, clearTableSelection: true });
} catch (err) { } catch (err) {
const error = err?.data || err; const error = err?.data || err;
const message = exceptionToErrorsArray(error); const message = exceptionToErrorsArray(error);
@ -146,17 +183,35 @@ export default {
<template> <template>
<Card :show-highlight-border="false"> <Card :show-highlight-border="false">
<template #title> <template #title>
{{ t('harvester.modal.migration.title') }} {{ t('harvester.modal.migration.vmMigrationTitle', { count: resources.length }) }}
</template> </template>
<template #body> <template #body>
<Banner <Banner
v-if="actionResource?.isCpuPinning" v-if="anyCpuPinning"
color="warning" color="warning"
:label="cpuPinningAlertMessage" :label="cpuPinningAlertMessage"
/> />
<p>
{{ t('harvester.modal.migration.selectedVMs') }}
</p>
<ul class="vm-list">
<li
v-for="group in vmsByNode"
:key="group.node"
>
{{ group.node || t('harvester.modal.migration.unknownNode') }}: {{ group.vms.join(', ') }}
<span
v-if="nodeName && group.node === nodeName"
class="already-on-target"
>
({{ t('harvester.modal.migration.alreadyOnTarget') }})
</span>
</li>
</ul>
<LabeledSelect <LabeledSelect
v-model:value="nodeName" v-model:value="nodeName"
class="mt-15"
:label="t('harvester.modal.migration.fields.nodeName.label')" :label="t('harvester.modal.migration.fields.nodeName.label')"
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')" :placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
:options="nodeNameList" :options="nodeNameList"
@ -183,7 +238,7 @@ export default {
<AsyncButton <AsyncButton
mode="apply" mode="apply"
:disabled="!nodeName" :disabled="!nodeName || allVmsOnTargetNode"
@click="apply" @click="apply"
/> />
</div> </div>
@ -201,4 +256,16 @@ export default {
justify-content: flex-end; justify-content: flex-end;
width: 100%; width: 100%;
} }
.already-on-target {
color: var(--warning);
font-style: italic;
}
.vm-list {
list-style: disc;
padding-left: 1.5em;
margin-bottom: 10px;
margin-top: 10px;
}
</style> </style>

View File

@ -108,6 +108,7 @@ export default {
<YamlEditor <YamlEditor
ref="yamlUser" ref="yamlUser"
v-model:value="config" v-model:value="config"
:mode="mode"
class="yaml-editor" class="yaml-editor"
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'" :editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
@onChanges="update" @onChanges="update"

View File

@ -8,6 +8,7 @@ import FileSelector, { createOnSelected } from '@shell/components/form/FileSelec
import { randomStr } from '@shell/utils/string'; import { randomStr } from '@shell/utils/string';
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import { getLoginAwareErrors } from '../utils/error';
export default { export default {
name: 'HarvesterEditKeypair', name: 'HarvesterEditKeypair',
@ -63,6 +64,14 @@ export default {
} }
}, },
computed: {
normalizedErrors() {
const message = this.t('harvester.virtualMachine.genericLoginError');
return getLoginAwareErrors(this.errors, message);
}
},
methods: { onKeySelected: createOnSelected('publicKey') }, methods: { onKeySelected: createOnSelected('publicKey') },
}; };
</script> </script>
@ -72,10 +81,9 @@ export default {
:done-route="doneRoute" :done-route="doneRoute"
:resource="value" :resource="value"
:mode="mode" :mode="mode"
:errors="errors" :errors="normalizedErrors"
:apply-hooks="applyHooks" :apply-hooks="applyHooks"
@finish="save" @finish="save"
@error="e=>errors=e"
> >
<div class="header mb-20"> <div class="header mb-20">
<FileSelector <FileSelector

View File

@ -9,6 +9,7 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise'; import { allHash } from '@shell/utils/promise';
import { NAMESPACE, NODE } from '@shell/config/types';
import { HCI } from '../types'; import { HCI } from '../types';
import { NETWORK_TYPE, L2VLAN_MODE } from '../config/types'; import { NETWORK_TYPE, L2VLAN_MODE } from '../config/types';
import { removeObject } from '@shell/utils/array'; import { removeObject } from '@shell/utils/array';
@ -20,6 +21,7 @@ const { ACCESS, TRUNK } = L2VLAN_MODE;
const AUTO = 'auto'; const AUTO = 'auto';
const MANUAL = 'manual'; const MANUAL = 'manual';
const KUBE_SYSTEM = 'kube-system';
export default { export default {
emits: ['update:value'], emits: ['update:value'],
@ -70,7 +72,12 @@ export default {
async fetch() { async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({ clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }) }); await allHash({
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
linkMonitors: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.LINK_MONITOR }),
});
}, },
created() { created() {
@ -199,6 +206,80 @@ export default {
} }
return this.type === UNTAGGED; return this.type === UNTAGGED;
},
showNicsTab() {
return this.isOverlayNetwork && this.value.metadata.namespace === KUBE_SYSTEM;
},
namespaceOptions() {
const ns = this.$store.getters['harvester/all'](NAMESPACE) || [];
// Allow users to select the "kube-system" namespace as the external subnet from Kube-OVN.
// This expects the provider network to be in the "kube-system" namespace for VPC NAT gateway functionality.
return ns
.filter((ns) => !ns.isSystem || ns.id === KUBE_SYSTEM)
.map((ns) => ({ name: ns.id }))
.sort((a, b) => a.name.localeCompare(b.name));
},
nodes() {
const inStore = this.$store.getters['currentProduct'].inStore;
const nodes = this.$store.getters[`${ inStore }/all`](NODE);
return nodes.filter((n) => n.isEtcd !== 'true');
},
nics() {
const inStore = this.$store.getters['currentProduct'].inStore;
const linkMonitor = this.$store.getters[`${ inStore }/byId`](HCI.LINK_MONITOR, 'nic') || {};
const linkStatus = linkMonitor?.status?.linkStatus || {};
const nodes = this.nodes.map((n) => n.id);
const out = [];
// Collect all nics from all nodes (for overlay, we select from all nodes)
Object.keys(linkStatus).map((nodeName) => {
if (nodes.includes(nodeName)) {
const nics = linkStatus[nodeName] || [];
nics.map((nic) => {
out.push({
...nic,
nodeName,
});
});
}
});
return out;
},
nicOptions() {
const out = [];
const seen = new Set();
(this.nics || []).forEach((nic) => {
if (!seen.has(nic.name)) {
seen.add(nic.name);
out.push({
label: nic.name,
value: nic.name,
});
}
});
return out.sort((a, b) => a.label.localeCompare(b.label));
},
master: {
get() {
return this.config?.master || '';
},
set(value) {
this.config.master = value;
}
} }
}, },
@ -214,6 +295,7 @@ export default {
this.config.ipam = {}; this.config.ipam = {};
this.config.bridge = ''; this.config.bridge = '';
delete this.config.provider; delete this.config.provider;
delete this.config.master;
delete this.config.server_socket; delete this.config.server_socket;
} }
}, },
@ -230,6 +312,13 @@ export default {
this.config.vlan = ''; this.config.vlan = '';
} }
}, },
'value.metadata.namespace'(newNamespace) {
// NIC selection is only valid for overlay in kube-system namespace.
if (newNamespace !== KUBE_SYSTEM) {
delete this.config.master;
this.value.spec.config = JSON.stringify({ ...this.config });
}
},
}, },
methods: { methods: {
@ -324,6 +413,10 @@ export default {
delete this.config.promiscMode; delete this.config.promiscMode;
delete this.config.vlan; delete this.config.vlan;
delete this.config.ipam; delete this.config.ipam;
if (this.value.metadata.namespace !== KUBE_SYSTEM) {
delete this.config.master;
}
} }
if (this.isUntaggedNetwork) { if (this.isUntaggedNetwork) {
@ -350,6 +443,7 @@ export default {
ref="nd" ref="nd"
:value="value" :value="value"
:mode="mode" :mode="mode"
:namespace-options="namespaceOptions"
@update:value="$emit('update:value', $event)" @update:value="$emit('update:value', $event)"
/> />
<Tabbed <Tabbed
@ -521,6 +615,25 @@ export default {
</div> </div>
</div> </div>
</Tab> </Tab>
<Tab
v-if="showNicsTab"
name="nics"
:label="t('harvester.network.tabs.nic')"
:weight="97"
class="bordered-table"
>
<div class="row mt-10">
<div class="col span-12">
<LabeledSelect
v-model:value="master"
:label="t('harvester.vlanConfig.uplink.nics.label')"
:placeholder="t('harvester.vlanConfig.uplink.nics.overlayWarning')"
:mode="mode"
:options="nicOptions"
/>
</div>
</div>
</Tab>
</Tabbed> </Tabbed>
</CruResource> </CruResource>
</template> </template>

View File

@ -3,8 +3,9 @@ import KeyValue from '@shell/components/form/KeyValue';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
import RadioGroup from '@components/Form/Radio/RadioGroup'; import RadioGroup from '@components/Form/Radio/RadioGroup';
import Checkbox from '@components/Form/Checkbox/Checkbox';
import { SECRET, LONGHORN } from '@shell/config/types'; import { SECRET, LONGHORN } from '@shell/config/types';
import { _CREATE, _VIEW } from '@shell/config/query-params'; import { _CREATE, _VIEW, _EDIT } from '@shell/config/query-params';
import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map'; import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map';
import { clone } from '@shell/utils/object'; import { clone } from '@shell/utils/object';
import { uniq } from '@shell/utils/array'; import { uniq } from '@shell/utils/array';
@ -16,7 +17,9 @@ const {
CSI_NODE_PUBLISH_SECRET_NAME, CSI_NODE_PUBLISH_SECRET_NAME,
CSI_NODE_PUBLISH_SECRET_NAMESPACE, CSI_NODE_PUBLISH_SECRET_NAMESPACE,
CSI_NODE_STAGE_SECRET_NAME, CSI_NODE_STAGE_SECRET_NAME,
CSI_NODE_STAGE_SECRET_NAMESPACE CSI_NODE_STAGE_SECRET_NAMESPACE,
CSI_NODE_EXPAND_SECRET_NAME,
CSI_NODE_EXPAND_SECRET_NAMESPACE
} = CSI_SECRETS; } = CSI_SECRETS;
export default { export default {
@ -27,6 +30,7 @@ export default {
LabeledSelect, LabeledSelect,
LabeledInput, LabeledInput,
RadioGroup, RadioGroup,
Checkbox,
}, },
props: { props: {
@ -57,7 +61,10 @@ export default {
}; };
} }
return { }; const hasExpandSecret = !!(this.value.parameters?.[CSI_NODE_EXPAND_SECRET_NAME] && this.value.parameters?.[CSI_NODE_EXPAND_SECRET_NAMESPACE]);
const volumeExpansionCheckBoxEnabled = this.realMode === _CREATE ? true : hasExpandSecret;
return { volumeExpansionCheckBoxEnabled };
}, },
computed: { computed: {
@ -98,8 +105,11 @@ export default {
}, []); }, []);
}, },
isEdit() {
return this.realMode === _EDIT;
},
isView() { isView() {
return this.mode === _VIEW; return this.realMode === _VIEW;
}, },
migratableOptions() { migratableOptions() {
@ -152,6 +162,10 @@ export default {
} }
}, },
enableOnlineExpansionVolumeEncryption() {
return this.value.expandOnlineEncryptedVolumeFeatureEnabled;
},
volumeEncryption: { volumeEncryption: {
set(neu) { set(neu) {
this.value['parameters'] = { this.value['parameters'] = {
@ -180,6 +194,11 @@ export default {
set(selectedSecret) { set(selectedSecret) {
const [namespace, name] = selectedSecret.split('/'); const [namespace, name] = selectedSecret.split('/');
const expandSecretParams = (this.enableOnlineExpansionVolumeEncryption && this.volumeExpansionCheckBoxEnabled) ? {
[CSI_NODE_EXPAND_SECRET_NAME]: name,
[CSI_NODE_EXPAND_SECRET_NAMESPACE]: namespace,
} : {};
this.value['parameters'] = { this.value['parameters'] = {
...this.value.parameters, ...this.value.parameters,
[CSI_PROVISIONER_SECRET_NAME]: name, [CSI_PROVISIONER_SECRET_NAME]: name,
@ -187,7 +206,8 @@ export default {
[CSI_NODE_STAGE_SECRET_NAME]: name, [CSI_NODE_STAGE_SECRET_NAME]: name,
[CSI_PROVISIONER_SECRET_NAMESPACE]: namespace, [CSI_PROVISIONER_SECRET_NAMESPACE]: namespace,
[CSI_NODE_PUBLISH_SECRET_NAMESPACE]: namespace, [CSI_NODE_PUBLISH_SECRET_NAMESPACE]: namespace,
[CSI_NODE_STAGE_SECRET_NAMESPACE]: namespace [CSI_NODE_STAGE_SECRET_NAMESPACE]: namespace,
...expandSecretParams,
}; };
} }
}, },
@ -240,6 +260,32 @@ export default {
} }
}, },
}, },
watch: {
volumeExpansionCheckBoxEnabled(enabled) {
const currentSecret = this.secret;
if (!currentSecret) {
return;
}
const [namespace, name] = currentSecret.split('/');
if (enabled && this.enableOnlineExpansionVolumeEncryption) {
this.value['parameters'] = {
...this.value.parameters,
[CSI_NODE_EXPAND_SECRET_NAME]: name,
[CSI_NODE_EXPAND_SECRET_NAMESPACE]: namespace,
};
} else {
const params = { ...this.value.parameters };
delete params[CSI_NODE_EXPAND_SECRET_NAME];
delete params[CSI_NODE_EXPAND_SECRET_NAMESPACE];
this.value['parameters'] = params;
}
},
},
}; };
</script> </script>
<template> <template>
@ -337,6 +383,16 @@ export default {
:mode="mode" :mode="mode"
/> />
</div> </div>
<div
v-if="enableOnlineExpansionVolumeEncryption"
class="col span-6 flex items-center mt-20"
>
<Checkbox
v-model:value="volumeExpansionCheckBoxEnabled"
:label="t('harvester.storage.volumeExpansionCheckbox')"
:disabled="isEdit || isView"
/>
</div>
</div> </div>
</template> </template>
<KeyValue <KeyValue

View File

@ -254,15 +254,24 @@ export default {
v-if="restoreNewVm" v-if="restoreNewVm"
v-model:value="restoreResource.spec.keepMacAddress" v-model:value="restoreResource.spec.keepMacAddress"
type="checkbox" type="checkbox"
class="check mb-20"
:label="t('harvester.backup.restore.keepMacAddress')" :label="t('harvester.backup.restore.keepMacAddress')"
/> />
<LabeledSelect <LabeledSelect
v-if="!restoreNewVm" v-if="!restoreNewVm"
v-model:value="deletionPolicy" v-model:value="deletionPolicy"
class="mb-20"
:label="t('harvester.backup.restore.deletePreviousVolumes')" :label="t('harvester.backup.restore.deletePreviousVolumes')"
:options="deletionPolicyOption" :options="deletionPolicyOption"
/> />
<Checkbox
v-model:value="restoreResource.spec.haltAfterRestore"
type="checkbox"
class="check mb-20"
:label="t('harvester.backup.restore.haltAfterRestore')"
/>
</div> </div>
<Footer <Footer

View File

@ -26,6 +26,7 @@ import CpuMemory from './kubevirt.io.virtualmachine/VirtualMachineCpuMemory';
import CpuModel from './kubevirt.io.virtualmachine/VirtualMachineCpuModel'; import CpuModel from './kubevirt.io.virtualmachine/VirtualMachineCpuModel';
import CloudConfig from './kubevirt.io.virtualmachine/VirtualMachineCloudConfig'; import CloudConfig from './kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
import SSHKey from './kubevirt.io.virtualmachine/VirtualMachineSSHKey'; import SSHKey from './kubevirt.io.virtualmachine/VirtualMachineSSHKey';
import Filesystem from './kubevirt.io.virtualmachine/VirtualMachineFilesystem';
export default { export default {
name: 'HarvesterEditVMTemplate', name: 'HarvesterEditVMTemplate',
@ -51,6 +52,7 @@ export default {
UnitInput, UnitInput,
Banner, Banner,
KeyValue, KeyValue,
Filesystem,
}, },
mixins: [CreateEditView, VM_MIXIN], mixins: [CreateEditView, VM_MIXIN],
@ -95,6 +97,10 @@ export default {
secretNamePrefix() { secretNamePrefix() {
return this.templateValue?.metadata?.name; return this.templateValue?.metadata?.name;
}, },
filesystemEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('supportFilesystem');
},
}, },
watch: { watch: {
@ -154,6 +160,7 @@ export default {
mounted() { mounted() {
this.imageId = this.diskRows[0]?.image || ''; this.imageId = this.diskRows[0]?.image || '';
this['filesystemRows'] = this.getFilesystemRows(this.value.spec.vm);
}, },
methods: { methods: {
@ -349,6 +356,19 @@ export default {
</template> </template>
</Tab> </Tab>
<Tab
v-if="filesystemEnabled"
name="filesystem"
:label="t('harvester.tab.filesystem')"
:weight="-8"
>
<Filesystem
v-model:value="filesystemRows"
:mode="mode"
:namespace="templateValue.metadata.namespace"
/>
</Tab>
<Tab <Tab
name="labels" name="labels"
:label="t('generic.labels')" :label="t('generic.labels')"

View File

@ -2,6 +2,7 @@
import Footer from '@shell/components/form/Footer'; import Footer from '@shell/components/form/Footer';
import { RadioGroup } from '@components/Form/Radio'; import { RadioGroup } from '@components/Form/Radio';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
import Checkbox from '@components/Form/Checkbox/Checkbox';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise'; import { allHash } from '@shell/utils/promise';
@ -32,6 +33,7 @@ const createObject = {
export default { export default {
name: 'CreateRestore', name: 'CreateRestore',
components: { components: {
Checkbox,
Footer, Footer,
RadioGroup, RadioGroup,
LabeledInput, LabeledInput,
@ -249,9 +251,17 @@ export default {
<LabeledSelect <LabeledSelect
v-if="!restoreNewVm" v-if="!restoreNewVm"
v-model:value="deletionPolicy" v-model:value="deletionPolicy"
class="mb-20"
:label="t('harvester.backup.restore.deletePreviousVolumes')" :label="t('harvester.backup.restore.deletePreviousVolumes')"
:options="deletionPolicyOption" :options="deletionPolicyOption"
/> />
<Checkbox
v-model:value="restoreResource.spec.haltAfterRestore"
type="checkbox"
class="check mb-20"
:label="t('harvester.backup.restore.haltAfterRestore')"
/>
</div> </div>
<Footer <Footer

View File

@ -0,0 +1,190 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../types';
export default {
emits: ['update:value'],
components: {
CruResource,
NameNsDescription,
ResourceTabs,
Tab,
LabeledInput,
LabeledSelect,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
return {
eip: this.value?.spec?.eip || '',
externalPort: this.value?.spec?.externalPort || '',
internalIp: this.value?.spec?.internalIp || '',
internalPort: this.value?.spec?.internalPort || '',
protocol: this.value?.spec?.protocol || '',
};
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({ eips: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.IPTABLES_EIP }) });
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
eipOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const eips = this.$store.getters[`${ inStore }/all`](HCI.IPTABLES_EIP) || [];
return eips.map((eip) => ({
label: eip.id,
value: eip.id,
}));
},
protocolOptions() {
return [
{ label: 'TCP', value: 'tcp' },
{ label: 'UDP', value: 'udp' },
];
},
},
methods: {
updateBeforeSave() {
if (!this.value.spec) {
this.value.spec = {};
}
this.value.spec.eip = this.eip;
this.value.spec.externalPort = this.externalPort;
this.value.spec.internalIp = this.internalIp;
this.value.spec.internalPort = this.internalPort;
this.value.spec.protocol = this.protocol;
},
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="basic"
:label="t('generic.basic')"
:weight="99"
>
<div class="mt-20">
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="eip"
class="mb-20"
:options="eipOptions"
:mode="mode"
:label="t('harvester.dnat.eip.label')"
:placeholder="t('harvester.dnat.eip.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value="externalPort"
class="mb-20"
:mode="mode"
:label="t('harvester.dnat.externalPort.label')"
:placeholder="t('harvester.dnat.externalPort.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value="internalIp"
class="mb-20"
:mode="mode"
:label="t('harvester.dnat.internalIp.label')"
:placeholder="t('harvester.dnat.internalIp.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value="internalPort"
class="mb-20"
:mode="mode"
:label="t('harvester.dnat.internalPort.label')"
:placeholder="t('harvester.dnat.internalPort.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="protocol"
class="mb-20"
:options="protocolOptions"
:mode="mode"
:label="t('harvester.dnat.protocol.label')"
:placeholder="t('harvester.dnat.protocol.placeholder')"
required
/>
</div>
</div>
</div>
</Tab>
</ResourceTabs>
</CruResource>
</template>

View File

@ -0,0 +1,168 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../types';
export default {
emits: ['update:value'],
components: {
CruResource,
NameNsDescription,
ResourceTabs,
Tab,
LabeledInput,
LabeledSelect,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
return {
natGwDp: this.value?.spec?.natGwDp || '',
externalSubnet: this.value?.spec?.externalSubnet || '',
v4ip: this.value?.spec?.v4ip || '',
};
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({
natGateways: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC_NAT_GATEWAY }),
subnets: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SUBNET }),
});
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
natGatewayOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const natGateways = this.$store.getters[`${ inStore }/all`](HCI.VPC_NAT_GATEWAY) || [];
return natGateways.map((gw) => ({
label: gw.id,
value: gw.id,
}));
},
subnetOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const subnets = this.$store.getters[`${ inStore }/all`](HCI.SUBNET) || [];
return subnets.map((subnet) => ({
label: subnet.id,
value: subnet.id,
}));
},
},
methods: {
updateBeforeSave() {
if (!this.value.spec) {
this.value.spec = {};
}
this.value.spec.natGwDp = this.natGwDp;
this.value.spec.externalSubnet = this.externalSubnet;
this.value.spec.v4ip = this.v4ip;
},
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="basic"
:label="t('generic.basic')"
:weight="99"
>
<div class="mt-20">
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="natGwDp"
class="mb-20"
:options="natGatewayOptions"
:mode="mode"
:label="t('harvester.externalIP.natGateway.label')"
:placeholder="t('harvester.externalIP.natGateway.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="externalSubnet"
class="mb-20"
:options="subnetOptions"
:label="t('harvester.externalIP.externalSubnet.label')"
:placeholder="t('harvester.externalIP.externalSubnet.placeholder')"
:mode="mode"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value="v4ip"
class="mb-20"
:label="t('harvester.externalIP.v4ip.label')"
:placeholder="t('harvester.externalIP.v4ip.placeholder')"
:mode="mode"
required
/>
</div>
</div>
</div>
</Tab>
</ResourceTabs>
</CruResource>
</template>

View File

@ -0,0 +1,140 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../types';
export default {
emits: ['update:value'],
components: {
CruResource,
NameNsDescription,
ResourceTabs,
Tab,
LabeledInput,
LabeledSelect,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
return {
eip: this.value?.spec?.eip || '',
internalCIDR: this.value?.spec?.internalCIDR || '',
};
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({ eips: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.IPTABLES_EIP }) });
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
eipOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const eips = this.$store.getters[`${ inStore }/all`](HCI.IPTABLES_EIP) || [];
return eips.map((eip) => ({
label: eip.id,
value: eip.id,
}));
},
},
methods: {
updateBeforeSave() {
if (!this.value.spec) {
this.value.spec = {};
}
this.value.spec.eip = this.eip;
this.value.spec.internalCIDR = this.internalCIDR;
},
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="basic"
:label="t('generic.basic')"
:weight="99"
>
<div class="mt-20">
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="eip"
class="mb-20"
:options="eipOptions"
:mode="mode"
:label="t('harvester.snat.eip.label')"
:placeholder="t('harvester.snat.eip.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value="internalCIDR"
class="mb-20"
:mode="mode"
:label="t('harvester.snat.internalCIDR.label')"
:placeholder="t('harvester.snat.internalCIDR.placeholder')"
required
/>
</div>
</div>
</div>
</Tab>
</ResourceTabs>
</CruResource>
</template>

View File

@ -0,0 +1,384 @@
<script>
import CruResource from '@shell/components/CruResource';
import ArrayList from '@shell/components/form/ArrayList';
import InfoBox from '@shell/components/InfoBox';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import ArrayListSelect from '@shell/components/form/ArrayListSelect';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { NODE } from '@shell/config/types';
import { HCI } from '../types';
export default {
emits: ['update:value'],
components: {
CruResource,
ArrayList,
InfoBox,
NameNsDescription,
ResourceTabs,
Tab,
LabeledSelect,
ArrayListSelect,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
return {
defaultInterface: this.value?.spec?.defaultInterface || '',
excludedNodes: this.value?.spec?.excludeNodes || [],
customInterfaces: this.value?.spec?.customInterfaces || [],
};
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
linkMonitors: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.LINK_MONITOR }),
});
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
nodes() {
const inStore = this.$store.getters['currentProduct'].inStore;
const nodes = this.$store.getters[`${ inStore }/all`](NODE);
return nodes.filter((n) => n.isEtcd !== 'true');
},
nics() {
const inStore = this.$store.getters['currentProduct'].inStore;
const linkMonitor = this.$store.getters[`${ inStore }/byId`](HCI.LINK_MONITOR, 'nic') || {};
const linkStatus = linkMonitor?.status?.linkStatus || {};
const nodes = this.nodes.map((n) => n.id);
const out = [];
// Collect all nics from all nodes
Object.keys(linkStatus).map((nodeName) => {
if (nodes.includes(nodeName)) {
const nics = linkStatus[nodeName] || [];
nics.map((nic) => {
out.push({
...nic,
nodeName,
});
});
}
});
return out;
},
nicOptions() {
const out = [];
const seen = new Set();
(this.nics || []).forEach((nic) => {
if (!seen.has(nic.name)) {
seen.add(nic.name);
out.push({
label: nic.name,
value: nic.name,
});
}
});
return out.sort((a, b) => a.label.localeCompare(b.label));
},
nodeOptions() {
return this.nodes.map((node) => ({
label: node.id,
value: node.id,
}));
},
},
methods: {
removeCustomInterface(index) {
this.customInterfaces.splice(index, 1);
},
updateBeforeSave() {
if (!this.value.spec) {
this.value.spec = {};
}
this.value.spec.defaultInterface = this.defaultInterface;
this.value.spec.excludeNodes = this.excludedNodes;
this.value.spec.customInterfaces = (this.customInterfaces || [])
.filter((item) => item?.interface || (item?.nodes || []).length)
.map((item) => ({
interface: item.interface || '',
nodes: (item.nodes || []).filter((node) => !!node),
}))
.filter((item) => item.interface && item.nodes.length > 0);
},
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="Interfaces"
label="Interfaces"
:weight="99"
>
<LabeledSelect
v-model:value="defaultInterface"
class="mb-20"
required
:options="nicOptions"
:mode="mode"
:label="t('harvester.providerNetwork.defaultInterface.label')"
:placeholder="t('harvester.providerNetwork.defaultInterface.placeholder')"
/>
<hr class="section-divider" />
<ArrayList
v-model:value="customInterfaces"
class="mb-20 custom-interface-list"
:mode="mode"
:title="t('harvester.providerNetwork.customInterfaces.label')"
:protip="false"
:remove-allowed="false"
:initial-empty-row="true"
:default-add-value="{ interface: '', nodes: [] }"
>
<template #add="{ add }">
<div class="custom-interface-primary-add">
<button
type="button"
class="btn role-primary"
:disabled="mode === 'view'"
@click="add"
>
{{ t('harvester.providerNetwork.customInterfaces.addLabel') }}
</button>
</div>
</template>
<template #column-headers>
<div class="row custom-interface-header">
<div class="col span-6">
{{ t('harvester.providerNetwork.customInterfaces.interface.label') }}
</div>
<div class="col span-6">
{{ t('harvester.providerNetwork.customInterfaces.nodes.label') }}
</div>
</div>
</template>
<template #columns="scope">
<InfoBox class="custom-interface-box">
<button
v-if="mode !== 'view'"
type="button"
class="role-link btn btn-sm remove"
@click="removeCustomInterface(scope.i)"
>
<i class="icon icon-x" />
</button>
<div class="custom-interface-content">
<div class="row custom-interface-row interface-row">
<div class="col span-12 interface-col">
<h3 class="mb-10">
{{ t('harvester.providerNetwork.customInterfaces.interface.label') }}
</h3>
<LabeledSelect
v-model:value="scope.row.value.interface"
class="mb-20"
:label="''"
:options="nicOptions"
:mode="mode"
:placeholder="t('harvester.providerNetwork.customInterfaces.interface.placeholder')"
/>
</div>
</div>
<div class="row custom-interface-row nodes-row">
<div class="col span-12">
<ArrayListSelect
v-model:value="scope.row.value.nodes"
:options="nodeOptions"
:mode="mode"
:disabled="mode === 'view'"
:enable-default-add-value="false"
:array-list-props="{
addLabel: t('harvester.providerNetwork.customInterfaces.nodes.addLabel'),
initialEmptyRow: true,
title: t('harvester.providerNetwork.customInterfaces.nodes.label'),
required: false,
protip: false,
}"
:select-props="{
placeholder: t('harvester.providerNetwork.customInterfaces.nodes.placeholder'),
disabled: mode === 'view',
}"
>
<template #add="{ add }">
<div class="custom-interface-add">
<button
type="button"
class="btn role-tertiary add"
:disabled="mode === 'view'"
@click="add"
>
{{ t('harvester.providerNetwork.customInterfaces.nodes.addLabel') }}
</button>
</div>
</template>
</ArrayListSelect>
</div>
</div>
</div>
</InfoBox>
</template>
</ArrayList>
</Tab>
<Tab
name="excludedNodes"
:label="t('harvester.providerNetwork.excludedNodes.label')"
:weight="98"
>
<div class="row">
<div class="col span-12">
<ArrayListSelect
v-model:value="excludedNodes"
:options="nodeOptions"
:disabled="mode === 'view'"
:mode="mode"
:enable-default-add-value="false"
:array-list-props="{
addLabel: t('harvester.providerNetwork.excludedNodes.addLabel'),
initialEmptyRow: true,
required: false,
protip: false,
}"
:select-props="{
placeholder: t('harvester.providerNetwork.excludedNodes.placeholder'),
disabled: mode === 'view',
}"
/>
</div>
</div>
</Tab>
</ResourceTabs>
</CruResource>
</template>
<style lang="scss" scoped>
.section-divider {
border: none;
border-top: 1px solid var(--border);
margin: 10px 0 20px;
}
.custom-interface-header {
margin-bottom: 10px;
font-weight: 600;
}
.custom-interface-row {
align-items: flex-start;
}
.interface-row {
width: calc(100% - 90px);
margin-bottom: 10px;
}
.nodes-row {
align-items: flex-start;
}
.custom-interface-add {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
:deep(.nodes-row .array-list-select .box) {
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
}
:deep(.nodes-row .array-list-select .box .remove) {
align-self: center;
}
.custom-interface-primary-add {
max-width: 100%;
}
.custom-interface-box {
position: relative;
width: 100%;
padding: 20px;
margin-bottom: 5px;
}
:deep(.custom-interface-list .box) {
grid-template-columns: 1fr;
}
.remove {
position: absolute;
top: 10px;
right: 10px;
z-index: 1;
padding: 0;
}
</style>

View File

@ -43,6 +43,7 @@ export default {
created() { created() {
const vpc = this.$route.query.vpc || ''; const vpc = this.$route.query.vpc || '';
const enableDHCP = this.value?.spec?.enableDHCP || false; const enableDHCP = this.value?.spec?.enableDHCP || false;
const natOutgoing = this.value?.spec?.natOutgoing || false;
set(this.value.spec, 'enableDHCP', enableDHCP); set(this.value.spec, 'enableDHCP', enableDHCP);
set(this.value, 'spec', this.value.spec || { set(this.value, 'spec', this.value.spec || {
@ -50,10 +51,11 @@ export default {
protocol: NETWORK_PROTOCOL.IPv4, protocol: NETWORK_PROTOCOL.IPv4,
provider: '', provider: '',
vpc, vpc,
gatewayIP: '', gateway: '',
excludeIps: [], excludeIps: [],
private: false, private: false,
enableDHCP, enableDHCP,
natOutgoing,
acls: [] acls: []
}); });
}, },
@ -64,6 +66,7 @@ export default {
const hash = { const hash = {
vpc: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }), vpc: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
nad: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }), nad: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }),
vlans: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN }),
}; };
await allHash(hash); await allHash(hash);
@ -129,6 +132,20 @@ export default {
label: n.id, label: n.id,
value: n.id, value: n.id,
})); }));
},
natOutgoingDisabled() {
// Disable the NAT Outgoing option when the subnet belongs to the ovn-cluster VPC and its name is join or ovn-default.
return this.value?.spec?.vpc === 'ovn-cluster' && ['join', 'ovn-default'].includes(this.value?.metadata?.name);
},
vlanOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const vlans = this.$store.getters[`${ inStore }/all`](HCI.VLAN) || [];
return vlans.map((vlan) => ({
label: vlan.id,
value: vlan.id,
}));
} }
}, },
@ -264,6 +281,16 @@ export default {
:mode="mode" :mode="mode"
/> />
</div> </div>
<div class="col span-6">
<LabeledSelect
v-model:value="value.spec.vlan"
class="mb-20"
:options="vlanOptions"
:placeholder="t('harvester.subnet.vlan.placeholder')"
:label="t('harvester.subnet.vlan.label')"
:mode="mode"
/>
</div>
</div> </div>
<div class="row mt-20"> <div class="row mt-20">
<div class="col span-6"> <div class="col span-6">
@ -304,6 +331,20 @@ export default {
</Banner> </Banner>
</div> </div>
</div> </div>
<div class="row mt-20">
<div class="col span-6">
<RadioGroup
v-model:value="value.spec.natOutgoing"
name="enableExternalConnectivity"
:disabled="natOutgoingDisabled"
:options="[true, false]"
:label="t('harvester.subnet.externalConnectivity.label')"
:labels="[t('generic.enabled'), t('generic.disabled')]"
:mode="mode"
:tooltip="t('harvester.subnet.externalConnectivity.tooltip')"
/>
</div>
</div>
<div class="row mt-20"> <div class="row mt-20">
<div class="col span-6"> <div class="col span-6">
<RadioGroup <RadioGroup

View File

@ -0,0 +1,146 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../types';
export default {
emits: ['update:value'],
components: {
CruResource,
NameNsDescription,
ResourceTabs,
Tab,
LabeledInput,
LabeledSelect,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
return {
vlanId: this.value?.spec?.id || '',
provider: this.value?.spec?.provider || '',
};
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({ providerNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.PROVIDER_NETWORK }) });
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
providerNetworks() {
const inStore = this.$store.getters['currentProduct'].inStore;
const providerNetworks = this.$store.getters[`${ inStore }/all`](HCI.PROVIDER_NETWORK) || [];
return providerNetworks.map((pn) => ({
label: pn.id,
value: pn.id,
}));
},
},
methods: {
updateBeforeSave() {
if (!this.value.spec) {
this.value.spec = {};
}
if (this.vlanId !== '') {
this.value.spec.id = Number(this.vlanId);
}
this.value.spec.provider = this.provider;
},
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="basic"
:label="t('generic.basic')"
:weight="99"
>
<div class="mt-20">
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value.number="vlanId"
class="mb-20"
type="number"
:min="1"
:max="4094"
:label="t('harvester.vlan.id.label')"
:placeholder="t('harvester.vlan.id.placeholder')"
:mode="mode"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="provider"
class="mb-20"
:options="providerNetworks"
:mode="mode"
:label="t('harvester.vlan.provider.label')"
:placeholder="t('harvester.vlan.provider.placeholder')"
required
/>
</div>
</div>
</div>
</Tab>
</ResourceTabs>
</CruResource>
</template>

View File

@ -0,0 +1,246 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import ArrayListSelect from '@shell/components/form/ArrayListSelect';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { HCI as HCI_ANNOTATIONS } from '@pkg/config/labels-annotations';
import { HCI } from '../types';
export default {
emits: ['update:value'],
components: {
CruResource,
NameNsDescription,
ResourceTabs,
Tab,
LabeledInput,
LabeledSelect,
ArrayListSelect,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
const internalTenantNetwork = this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.CNI_NETWORKS] || '';
return {
internalTenantNetwork,
vpc: this.value?.spec?.vpc || '',
subnet: this.value?.spec?.subnet || '',
lanIp: this.value?.spec?.lanIp || '',
externalSubnets: this.value?.spec?.externalSubnets || [],
};
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({
vpcs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
subnets: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SUBNET }),
vmNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.NETWORK_ATTACHMENT }),
});
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
vpcOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const vpcs = this.$store.getters[`${ inStore }/all`](HCI.VPC) || [];
return vpcs.map((vpc) => ({
label: vpc.id,
value: vpc.id,
}));
},
subnetOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const subnets = this.$store.getters[`${ inStore }/all`](HCI.SUBNET) || [];
return subnets.map((subnet) => ({
label: subnet.id,
value: subnet.id,
}));
},
vmNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const vmNetworks = this.$store.getters[`${ inStore }/all`](HCI.NETWORK_ATTACHMENT) || [];
return vmNetworks.map((network) => ({
label: network.id,
value: network.id,
}));
},
},
methods: {
updateBeforeSave() {
if (!this.value.spec) {
this.value.spec = {};
}
if (!this.value.metadata) {
this.value.metadata = {};
}
if (!this.value.metadata.annotations) {
this.value.metadata.annotations = {};
}
this.value.spec.vpc = this.vpc;
this.value.spec.subnet = this.subnet;
this.value.spec.lanIp = this.lanIp;
this.value.spec.externalSubnets = (this.externalSubnets || []).filter((subnet) => !!subnet);
if (this.internalTenantNetwork) {
this.value.metadata.annotations[HCI_ANNOTATIONS.CNI_NETWORKS] = this.internalTenantNetwork;
} else {
delete this.value.metadata.annotations[HCI_ANNOTATIONS.CNI_NETWORKS];
}
},
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="basic"
:label="t('generic.basic')"
:weight="99"
>
<div class="mt-20">
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="internalTenantNetwork"
class="mb-20"
required
:options="vmNetworkOptions"
:mode="mode"
:label="t('harvester.natGateway.internalTenantNetwork.label')"
:placeholder="t('harvester.natGateway.internalTenantNetwork.placeholder')"
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="vpc"
class="mb-20"
:options="vpcOptions"
:mode="mode"
:label="t('harvester.natGateway.vpc.label')"
:placeholder="t('harvester.natGateway.vpc.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model:value="subnet"
class="mb-20"
:options="subnetOptions"
:mode="mode"
:label="t('harvester.natGateway.subnet.label')"
:placeholder="t('harvester.natGateway.subnet.placeholder')"
required
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value="lanIp"
class="mb-20"
:mode="mode"
:label="t('harvester.natGateway.lanIp.label')"
:placeholder="t('harvester.natGateway.lanIp.placeholder')"
required
/>
</div>
</div>
</div>
</Tab>
<Tab
name="externalSubnets"
:label="t('harvester.natGateway.externalSubnets.label')"
:weight="98"
>
<div class="mt-20">
<div class="row">
<div class="col span-12">
<ArrayListSelect
v-model:value="externalSubnets"
:mode="mode"
:disabled="mode === 'view'"
required
:options="subnetOptions"
:enable-default-add-value="false"
:array-list-props="{
addLabel: t('harvester.natGateway.externalSubnets.addLabel'),
title: t('harvester.natGateway.subnet.label'),
initialEmptyRow: true,
required: true,
protip: false,
}"
:select-props="{
placeholder: t('harvester.natGateway.externalSubnets.placeholder'),
disabled: mode === 'view',
}"
/>
</div>
</div>
</div>
</Tab>
</ResourceTabs>
</CruResource>
</template>

View File

@ -143,6 +143,7 @@ export default {
<YamlEditor <YamlEditor
ref="yaml" ref="yaml"
v-model:value="yamlScript" v-model:value="yamlScript"
:mode="mode"
class="yaml-editor" class="yaml-editor"
:editor-mode="editorMode" :editor-mode="editorMode"
/> />

View File

@ -0,0 +1,310 @@
<script>
import { mapGetters } from 'vuex';
import { Banner } from '@components/Banner';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
import { CONFIG_MAP, SECRET, SERVICE_ACCOUNT } from '@shell/config/types';
import { _VIEW } from '@shell/config/query-params';
import CopyToClipboard from '@shell/components/CopyToClipboard';
import MessageLink from '@shell/components/MessageLink';
import { FILESYSTEM_SOURCE_TYPE } from '@pkg/harvester/config/types';
const MAX_FILESYSTEMS = 3;
const { CONFIGMAP: FS_TYPE_CONFIGMAP, SECRET: FS_TYPE_SECRET, SERVICEACCOUNT: FS_TYPE_SERVICEACCOUNT } = FILESYSTEM_SOURCE_TYPE;
const DEFAULT_VOLUME_NAMES = {
[FS_TYPE_CONFIGMAP]: 'appconfigfs',
[FS_TYPE_SECRET]: 'appsecretfs',
[FS_TYPE_SERVICEACCOUNT]: 'appserviceaccountfs',
};
const FS_TYPE_OPTIONS = [
{ label: 'ConfigMap', value: FS_TYPE_CONFIGMAP },
{ label: 'Secret', value: FS_TYPE_SECRET },
{ label: 'ServiceAccount', value: FS_TYPE_SERVICEACCOUNT },
];
function emptyRow() {
return {
fsType: FS_TYPE_CONFIGMAP,
volumeName: DEFAULT_VOLUME_NAMES[FS_TYPE_CONFIGMAP],
resourceName: '',
};
}
export default {
name: 'VirtualMachineFilesystem',
emits: ['update:value'],
components: {
Banner,
LabeledSelect,
LabeledInput,
CopyToClipboard,
MessageLink,
},
props: {
mode: {
type: String,
default: 'create',
},
namespace: {
type: String,
default: '',
},
value: {
type: Array,
default: () => [],
},
},
data() {
return { rows: this.value.length > 0 ? this.value.map((r) => ({ ...r })) : [emptyRow()] };
},
watch: {
value(newVal) {
if (newVal) {
const incoming = JSON.stringify(newVal);
const current = JSON.stringify(this.rows);
if (incoming !== current) {
this.rows = newVal.map((r) => ({ ...r }));
}
}
},
rows: {
deep: true,
handler(val) {
this.$emit('update:value', val.map((r) => ({ ...r })));
},
},
},
computed: {
...mapGetters({ t: 'i18n/t' }),
inStore() {
return this.$store.getters['currentProduct'].inStore;
},
configMaps() {
return this.$store.getters[`${ this.inStore }/all`](CONFIG_MAP)
.filter((cm) => !this.namespace || cm.metadata.namespace === this.namespace)
.map((cm) => ({ label: cm.metadata.name, value: cm.metadata.name }));
},
secrets() {
return this.$store.getters[`${ this.inStore }/all`](SECRET)
.filter((s) => !this.namespace || s.metadata.namespace === this.namespace)
.map((s) => ({ label: s.metadata.name, value: s.metadata.name }));
},
serviceAccounts() {
return this.$store.getters[`${ this.inStore }/all`](SERVICE_ACCOUNT)
.filter((sa) => !this.namespace || sa.metadata.namespace === this.namespace)
.map((sa) => ({ label: sa.metadata.name, value: sa.metadata.name }));
},
canAddRow() {
return this.rows.length < MAX_FILESYSTEMS;
},
isView() {
return this.mode === _VIEW;
},
completedRows() {
return this.rows.filter((r) => r.fsType && r.volumeName && r.resourceName);
},
allMountCommands() {
return this.completedRows.map((r) => this.mountCommands(r)).join('\n');
},
},
methods: {
fsTypeOptions(currentIndex) {
const usedTypes = this.rows
.filter((_, i) => i !== currentIndex)
.map((r) => r.fsType);
return FS_TYPE_OPTIONS.filter((opt) => !usedTypes.includes(opt.value));
},
resourceOptions(fsType) {
if (fsType === FS_TYPE_CONFIGMAP) return this.configMaps;
if (fsType === FS_TYPE_SECRET) return this.secrets;
if (fsType === FS_TYPE_SERVICEACCOUNT) return this.serviceAccounts;
return [];
},
onFsTypeChange(row, newType) {
row.fsType = newType;
row.volumeName = DEFAULT_VOLUME_NAMES[newType] || '';
row.resourceName = '';
},
addRow() {
if (this.canAddRow) {
const usedTypes = this.rows.map((r) => r.fsType);
const nextType = FS_TYPE_OPTIONS.find((opt) => !usedTypes.includes(opt.value))?.value || FS_TYPE_CONFIGMAP;
this.rows.push({
fsType: nextType,
volumeName: DEFAULT_VOLUME_NAMES[nextType] || '',
resourceName: '',
});
}
},
removeRow(index) {
this.rows.splice(index, 1);
},
mountCommands(row) {
const vol = row.volumeName || '<volume-name>';
return `- mkdir -p /mnt/${ vol }\n- mount -t virtiofs ${ vol } /mnt/${ vol }`;
},
},
};
</script>
<template>
<div class="vm-filesystem">
<p class="mb-20">
{{ t('harvester.virtualMachine.filesystem.description') }}
</p>
<div
v-for="(row, index) in rows"
:key="index"
class="filesystem-row mb-15"
>
<div class="row">
<div class="col span-3">
<LabeledSelect
:value="row.fsType"
:label="t('harvester.virtualMachine.filesystem.type')"
:options="fsTypeOptions(index)"
:mode="mode"
required
@update:value="onFsTypeChange(row, $event)"
/>
</div>
<div class="col span-3">
<LabeledInput
v-model:value="row.volumeName"
:label="t('harvester.virtualMachine.filesystem.volume')"
:mode="mode"
required
/>
</div>
<div class="col span-5">
<LabeledSelect
v-model:value="row.resourceName"
:label="t('harvester.virtualMachine.filesystem.resource')"
:options="resourceOptions(row.fsType)"
:mode="mode"
required
/>
</div>
<div
v-if="!isView"
class="col span-1 remove-col"
>
<button
type="button"
class="btn role-link remove-btn"
@click="removeRow(index)"
>
{{ t('generic.remove') }}
</button>
</div>
</div>
</div>
<Banner
v-if="completedRows.length > 0 && mode === 'create'"
color="warning"
class="mt-10"
>
<div>
<MessageLink
:to="{ hash: '#advanced' }"
prefix-label="harvester.virtualMachine.filesystem.mountBannerHint"
middle-label="harvester.virtualMachine.filesystem.mountBannerHintLink"
suffix-label="harvester.virtualMachine.filesystem.mountBannerHintSuffix"
/>
<div class="pre-wrapper mt-10">
<pre class="mt-5 mb-0">{{ allMountCommands }}</pre>
<CopyToClipboard
:text="allMountCommands"
:show-label="false"
class="icon-btn"
action-color="bg-transparent"
/>
</div>
</div>
</Banner>
<button
v-if="!isView && canAddRow"
type="button"
class="btn role-tertiary add"
@click="addRow"
>
{{ t('harvester.virtualMachine.filesystem.add') }}
</button>
</div>
</template>
<style lang="scss" scoped>
.vm-filesystem {
padding: 10px 0;
}
.filesystem-row {
border-bottom: 1px solid var(--border);
padding-bottom: 15px;
}
.remove-col {
display: flex;
align-items: center;
justify-content: center;
}
.remove-btn {
padding: 0;
}
.pre-wrapper {
position: relative;
pre {
padding-right: 36px;
}
.icon-btn {
position: absolute;
top: 0px;
right: 5px;
padding: 2px 4px;
line-height: 1;
&:active {
background-color: var(--success) !important;
}
}
}
</style>

View File

@ -152,6 +152,10 @@ export default {
return !rows.find((device) => !device.passthroughClaim); return !rows.find((device) => !device.passthroughClaim);
}, },
canManageGroup(rows = []) {
return rows.length > 0 && rows.every((row) => row.canUpdate === true);
},
changeRows(filterRows, parentSriov) { changeRows(filterRows, parentSriov) {
this['filterRows'] = filterRows; this['filterRows'] = filterRows;
this['parentSriov'] = parentSriov; this['parentSriov'] = parentSriov;
@ -184,6 +188,10 @@ export default {
:ref="group.key" :ref="group.key"
v-trim-whitespace v-trim-whitespace
class="group-tab" class="group-tab"
>
<div
v-if="canManageGroup(group.rows)"
class="group-actions"
> >
<button <button
v-if="groupIsAllEnabled(group.rows)" v-if="groupIsAllEnabled(group.rows)"
@ -201,6 +209,7 @@ export default {
> >
{{ t('harvester.pci.enableGroup') }} {{ t('harvester.pci.enableGroup') }}
</button> </button>
</div>
<span v-clean-html="group.key" /> <span v-clean-html="group.key" />
</div> </div>
</template> </template>
@ -232,3 +241,9 @@ export default {
</template> </template>
</ResourceTable> </ResourceTable>
</template> </template>
<style lang="scss" scoped>
.group-actions {
display: inline;
}
</style>

View File

@ -52,31 +52,21 @@ export default {
} }
const selectedDevices = []; const selectedDevices = [];
const oldFormatDevices = [];
const vmDevices = this.value?.domain?.devices?.hostDevices || []; const vmDevices = this.value?.domain?.devices?.hostDevices || [];
const otherDevices = this.otherDevices(vmDevices).map(({ name }) => name);
const vmDeviceNames = vmDevices.map(({ name }) => name); const vmDeviceNames = vmDevices.map(({ name }) => name);
this.pciDevices.forEach((row) => { this.pciDevices.forEach((row) => {
row.allowDisable = !vmDeviceNames.includes(row.metadata.name); row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
}); });
vmDevices.forEach(({ name, deviceName }) => { vmDevices.forEach(({ name }) => {
const checkName = (deviceName || '').split('/')?.[1]; if (this.enabledDevices.find((device) => device?.metadata?.name === name)) {
if (checkName && name.includes(checkName) && !otherDevices.includes(name)) {
oldFormatDevices.push(name);
} else if (this.enabledDevices.find((device) => device?.metadata?.name === name)) {
selectedDevices.push(name); selectedDevices.push(name);
} }
}); });
if (oldFormatDevices.length > 0) {
this.oldFormatDevices = oldFormatDevices;
} else {
this.selectedDevices = selectedDevices; this.selectedDevices = selectedDevices;
}
}, },
data() { data() {
@ -87,7 +77,6 @@ export default {
selectedDevices: [], selectedDevices: [],
pciDeviceSchema: this.$store.getters['harvester/schemaFor'](HCI.PCI_DEVICE), pciDeviceSchema: this.$store.getters['harvester/schemaFor'](HCI.PCI_DEVICE),
showMatrix: false, showMatrix: false,
oldFormatDevices: [],
}; };
}, },
@ -199,11 +188,6 @@ export default {
}); });
}, },
oldFormatDevicesHTML() {
return this.oldFormatDevices.map((device) => {
return `<li>${ device }</li>`;
}).join('');
},
}, },
methods: { methods: {
@ -227,17 +211,6 @@ export default {
<template> <template>
<div> <div>
<div
v-if="oldFormatDevices.length > 0"
class="row"
>
<div class="col span-12">
<Banner color="warning">
<p v-clean-html="t('harvester.pci.oldFormatDevices.help', {oldFormatDevicesHTML}, true)" />
</Banner>
</div>
</div>
<div v-else>
<div class="row"> <div class="row">
<div class="col span-12"> <div class="col span-12">
<Banner color="info"> <Banner color="info">
@ -323,5 +296,4 @@ export default {
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>

View File

@ -10,6 +10,7 @@ import { _VIEW } from '@shell/config/query-params';
import { NAMESPACE } from '@shell/config/types'; import { NAMESPACE } from '@shell/config/types';
import { HCI } from '../../types'; import { HCI } from '../../types';
import { getLoginAwareErrors } from '../../utils/error';
const _NEW = '_NEW'; const _NEW = '_NEW';
@ -214,7 +215,9 @@ export default {
buttonCb(true); buttonCb(true);
this.cancel(); this.cancel();
} catch (err) { } catch (err) {
this.errors = [err.message]; const message = this.t('harvester.virtualMachine.genericLoginError');
this.errors = getLoginAwareErrors(err, message);
buttonCb(false); buttonCb(false);
} }
}, },

View File

@ -64,6 +64,12 @@ export default {
value: 'status.productID', value: 'status.productID',
sort: ['status.productID', 'status.vendorID'] sort: ['status.productID', 'status.vendorID']
}, },
{
name: 'classType',
labelKey: 'harvester.usb.classType',
value: 'status.classType',
sort: ['status.classType']
},
]; ];
if (!isSingleProduct) { if (!isSingleProduct) {
@ -107,6 +113,11 @@ export default {
} }
}); });
}, },
canManageGroup(rows = []) {
return rows.every((row) => row.canUpdate === true);
},
groupIsAllEnabled(rows = []) { groupIsAllEnabled(rows = []) {
return !rows.find((device) => !device.passthroughClaim); return !rows.find((device) => !device.passthroughClaim);
}, },
@ -146,6 +157,10 @@ export default {
:ref="group.key" :ref="group.key"
v-trim-whitespace v-trim-whitespace
class="group-tab" class="group-tab"
>
<div
v-if="canManageGroup(group.rows)"
class="group-actions"
> >
<button <button
v-if="groupIsAllEnabled(group.rows)" v-if="groupIsAllEnabled(group.rows)"
@ -163,6 +178,7 @@ export default {
> >
{{ t('harvester.usb.enableGroup') }} {{ t('harvester.usb.enableGroup') }}
</button> </button>
</div>
<span v-clean-html="group.key" /> <span v-clean-html="group.key" />
</div> </div>
</template> </template>
@ -175,3 +191,9 @@ export default {
</template> </template>
</ResourceTable> </ResourceTable>
</template> </template>
<style lang="scss" scoped>
.group-actions {
display: inline;
}
</style>

View File

@ -475,7 +475,6 @@ export default {
<button <button
type="button" type="button"
class="btn btn-sm bg-primary mr-15 mb-10" class="btn btn-sm bg-primary mr-15 mb-10"
:disabled="rows.length === 0"
@click="addVolume(SOURCE_TYPE.NEW)" @click="addVolume(SOURCE_TYPE.NEW)"
> >
{{ t('harvester.virtualMachine.volume.addVolume') }} {{ t('harvester.virtualMachine.volume.addVolume') }}

View File

@ -21,9 +21,7 @@ import { saferDump } from '@shell/utils/create-yaml';
import { exceptionToErrorsArray } from '@shell/utils/error'; import { exceptionToErrorsArray } from '@shell/utils/error';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { BEFORE_SAVE_HOOKS, AFTER_SAVE_HOOKS } from '@shell/mixins/child-hook'; import { BEFORE_SAVE_HOOKS, AFTER_SAVE_HOOKS } from '@shell/mixins/child-hook';
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import { parseVolumeClaimTemplates } from '@pkg/utils/vm'; import { parseVolumeClaimTemplates } from '@pkg/utils/vm';
import VM_MIXIN from '../../mixins/harvester-vm'; import VM_MIXIN from '../../mixins/harvester-vm';
import { HCI } from '../../types'; import { HCI } from '../../types';
@ -37,6 +35,7 @@ import Network from './VirtualMachineNetwork';
import Volume from './VirtualMachineVolume'; import Volume from './VirtualMachineVolume';
import SSHKey from './VirtualMachineSSHKey'; import SSHKey from './VirtualMachineSSHKey';
import Reserved from './VirtualMachineReserved'; import Reserved from './VirtualMachineReserved';
import Filesystem from './VirtualMachineFilesystem';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import MessageLink from '@shell/components/MessageLink'; import MessageLink from '@shell/components/MessageLink';
@ -72,6 +71,7 @@ export default {
Banner, Banner,
MessageLink, MessageLink,
UsbDevices, UsbDevices,
Filesystem,
}, },
mixins: [CreateEditView, VM_MIXIN], mixins: [CreateEditView, VM_MIXIN],
@ -91,6 +91,8 @@ export default {
const hostname = this.value.spec.template.spec.hostname || ''; const hostname = this.value.spec.template.spec.hostname || '';
const customizeDisplayName = !!(this.value.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME]);
return { return {
cloneVM, cloneVM,
count: 2, count: 2,
@ -101,12 +103,24 @@ export default {
isOpen: false, isOpen: false,
hostname, hostname,
isRestartImmediately, isRestartImmediately,
customizeDisplayName,
}; };
}, },
computed: { computed: {
...mapGetters({ t: 'i18n/t' }), ...mapGetters({ t: 'i18n/t' }),
// VM display name is stored as an annotation; bind a dedicated input to it
// so we don't have to mutate metadata.name (which would break the k8s PUT).
displayName: {
get() {
return this.value.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME] || '';
},
set(val) {
this.value.setAnnotation(HCI_ANNOTATIONS.VM_DISPLAY_NAME, val);
},
},
to() { to() {
return { return {
name: 'harvester-c-cluster-resource', name: 'harvester-c-cluster-resource',
@ -218,6 +232,9 @@ export default {
usbPassthroughEnabled() { usbPassthroughEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough'); return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
}, },
filesystemEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('supportFilesystem');
},
}, },
watch: { watch: {
@ -289,6 +306,12 @@ export default {
this.getInitConfig({ value: this.value, init: this.isCreate }); this.getInitConfig({ value: this.value, init: this.isCreate });
} }
}, },
customizeDisplayName(neu) {
if (!neu) {
this.value.setAnnotation(HCI_ANNOTATIONS.VM_DISPLAY_NAME, '');
}
},
}, },
created() { created() {
@ -321,6 +344,7 @@ export default {
const diskRows = this.getDiskRows(this.value); const diskRows = this.getDiskRows(this.value);
this['diskRows'] = diskRows; this['diskRows'] = diskRows;
this['filesystemRows'] = this.getFilesystemRows(this.value);
const templateId = this.$route.query.templateId; const templateId = this.$route.query.templateId;
const templateVersionId = this.$route.query.versionId; const templateVersionId = this.$route.query.versionId;
@ -610,6 +634,33 @@ export default {
</template> </template>
</NameNsDescription> </NameNsDescription>
<div v-if="isSingle">
<div class="row mb-20">
<div class="col span-12">
<Checkbox
v-model:value="customizeDisplayName"
class="check"
type="checkbox"
:label="t('harvester.virtualMachine.input.customizeDisplayName')"
:mode="mode"
/>
</div>
</div>
<div
v-if="customizeDisplayName"
class="row mb-20"
>
<div class="col span-6">
<LabeledInput
v-model:value="displayName"
:mode="mode"
:label="t('harvester.virtualMachine.input.displayName')"
:placeholder="t('harvester.virtualMachine.input.displayNamePlaceholder')"
/>
</div>
</div>
</div>
<Checkbox <Checkbox
v-if="isCreate" v-if="isCreate"
v-model:value="useTemplate" v-model:value="useTemplate"
@ -783,10 +834,23 @@ export default {
/> />
</Tab> </Tab>
<Tab
v-if="filesystemEnabled"
name="filesystem"
:label="t('harvester.tab.filesystem')"
:weight="-9"
>
<Filesystem
v-model:value="filesystemRows"
:mode="isCreate ? mode : 'view'"
:namespace="value.metadata.namespace"
/>
</Tab>
<Tab <Tab
name="labels" name="labels"
:label="t('generic.labels')" :label="t('generic.labels')"
:weight="-9" :weight="-10"
> >
<Banner color="info"> <Banner color="info">
<t k="harvester.virtualMachine.labels.banner" /> <t k="harvester.virtualMachine.labels.banner" />
@ -805,7 +869,7 @@ export default {
<Tab <Tab
name="instanceLabel" name="instanceLabel"
:label="t('harvester.tab.instanceLabel')" :label="t('harvester.tab.instanceLabel')"
:weight="-10" :weight="-11"
> >
<Banner color="info"> <Banner color="info">
<t k="harvester.virtualMachine.instanceLabels.banner" /> <t k="harvester.virtualMachine.instanceLabels.banner" />
@ -826,7 +890,7 @@ export default {
<Tab <Tab
name="annotations" name="annotations"
:label="t('harvester.tab.annotations')" :label="t('harvester.tab.annotations')"
:weight="-11" :weight="-12"
> >
<Banner color="info"> <Banner color="info">
<t k="harvester.virtualMachine.annotations.banner" /> <t k="harvester.virtualMachine.annotations.banner" />
@ -847,7 +911,7 @@ export default {
<Tab <Tab
name="advanced" name="advanced"
:label="t('harvester.tab.advanced')" :label="t('harvester.tab.advanced')"
:weight="-12" :weight="-13"
> >
<div class="row mb-20"> <div class="row mb-20">
<div class="col span-6"> <div class="col span-6">

View File

@ -0,0 +1,419 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import InfoBox from '@shell/components/InfoBox';
import MessageLink from '@shell/components/MessageLink';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
import { RadioGroup } from '@components/Form/Radio';
import { Checkbox } from '@components/Form/Checkbox';
import HarvesterNodeSelector from '../components/HarvesterNodeSelector';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { set } from '@shell/utils/object';
import { NODE } from '@shell/config/types';
import { HCI } from '../types';
import { ADD_ONS } from '../config/harvester-map';
const MODE_DHCP = 'dhcp';
const MODE_STATIC = 'static';
export default {
name: 'HarvesterHostNetworkConfigEditPage',
emits: ['update:value'],
components: {
CruResource,
NameNsDescription,
ResourceTabs,
Tab,
InfoBox,
MessageLink,
LabeledSelect,
LabeledInput,
RadioGroup,
Checkbox,
HarvesterNodeSelector,
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
hostNetworkConfigs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HOST_NETWORK_CONFIG }),
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
});
},
data() {
const networkMode = this.value?.spec?.mode || MODE_DHCP;
const ips = { ...(this.value?.spec?.ips || {}) };
if (!this.value.spec) {
set(this.value, 'spec', {});
}
return {
networkMode,
ips,
hasNodeSelector: !!this.value?.spec?.nodeSelector,
};
},
computed: {
modeOptions() {
return [
{ label: 'DHCP', value: MODE_DHCP },
{ label: 'Static', value: MODE_STATIC },
];
},
clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
return clusterNetworks.map((n) => {
const disabled = !n.isReady;
return {
label: disabled ? `${ n.id } (${ this.t('generic.notReady') })` : n.id,
value: n.id,
disabled,
};
});
},
nodes() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/all`](NODE) || [];
},
isStaticMode() {
return this.networkMode === MODE_STATIC;
},
underlay: {
get() {
return !!this.value?.spec?.underlay;
},
set(val) {
set(this.value, 'spec.underlay', val);
},
},
vlanID: {
get() {
return this.value?.spec?.vlanID;
},
set(val) {
set(this.value, 'spec.vlanID', val);
},
},
clusterNetwork: {
get() {
return this.value?.spec?.clusterNetwork;
},
set(val) {
set(this.value, 'spec.clusterNetwork', val);
},
},
underlayConflict() {
const inStore = this.$store.getters['currentProduct'].inStore;
const all = this.$store.getters[`${ inStore }/all`](HCI.HOST_NETWORK_CONFIG) || [];
const currentId = this.value?.id;
return all.find((c) => c.id !== currentId && c.spec?.underlay === true) || null;
},
kubeovnEnabled() {
const inStore = this.$store.getters['currentProduct'].inStore;
const addons = this.$store.getters[`${ inStore }/all`](HCI.ADD_ONS) || [];
return addons.find((a) => a.name === ADD_ONS.KUBEOVN_OPERATOR)?.spec?.enabled === true;
},
underlayDisabled() {
return !this.kubeovnEnabled || !!this.underlayConflict;
},
kubeovnAddonTo() {
return {
name: 'c-cluster-product-resource-namespace-id',
params: {
cluster: this.$route.params.cluster,
product: this.$store.getters['productId'],
resource: HCI.ADD_ONS,
namespace: 'kube-system',
id: ADD_ONS.KUBEOVN_OPERATOR,
},
query: { mode: 'edit' },
hash: '#basic',
};
},
},
watch: {
networkMode(neu) {
set(this.value, 'spec.mode', neu);
if (neu !== MODE_STATIC) {
if (this.value?.spec?.ips !== undefined) {
delete this.value.spec.ips;
}
this.ips = {};
}
},
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
methods: {
updateBeforeSave() {
set(this.value, 'spec.mode', this.networkMode);
if (this.isStaticMode) {
set(this.value, 'spec.ips', { ...this.ips });
}
},
updateIp(nodeName, val) {
this.ips = { ...this.ips, [nodeName]: val };
},
addNodeSelector() {
set(this.value.spec, 'nodeSelector', {
matchExpressions: [{
key: '', operator: 'In', values: []
}]
});
this.hasNodeSelector = true;
},
removeNodeSelector() {
delete this.value.spec.nodeSelector;
this.hasNodeSelector = false;
},
},
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:mode="mode"
:resource="value"
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@cancel="done"
@error="e => errors = e"
>
<NameNsDescription
:value="value"
:mode="mode"
:namespaced="false"
description-key="spec.description"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-conditions="false"
:need-related="false"
:need-events="false"
:side-tabs="true"
:mode="mode"
>
<Tab
name="basic"
:label="t('harvester.hostNetworkConfig.tabs.mode')"
:weight="99"
class="bordered-table"
>
<div class="row mb-20">
<div class="col span-6">
<RadioGroup
v-model:value="networkMode"
name="hostNetworkConfigMode"
:options="modeOptions"
:mode="mode"
:row="true"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledSelect
v-model:value="clusterNetwork"
:label="t('harvester.network.clusterNetwork.label')"
:options="clusterNetworkOptions"
:mode="mode"
required
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model:value.number="vlanID"
type="number"
required
:min="2"
:max="4094"
placeholder="e.g. 2 ~ 4094"
:label="t('harvester.hostNetworkConfig.vlanID.label')"
:mode="mode"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<Checkbox
v-model:value="underlay"
:label="t('harvester.hostNetworkConfig.underlay.label')"
:tooltip="t('harvester.hostNetworkConfig.underlay.tooltip')"
:mode="mode"
:disabled="underlayDisabled"
/>
<span
v-if="!kubeovnEnabled"
class="underlay-conflict-warning"
>
<i class="icon icon-warning" />
<MessageLink
:to="kubeovnAddonTo"
prefix-label="harvester.hostNetworkConfig.underlay.noKubeovn.prefix"
middle-label="harvester.hostNetworkConfig.underlay.noKubeovn.middle"
suffix-label="harvester.hostNetworkConfig.underlay.noKubeovn.suffix"
/>
</span>
<span
v-else-if="underlayConflict"
class="underlay-conflict-warning"
>
<i class="icon icon-warning" />
{{ t('harvester.hostNetworkConfig.underlay.conflict', { name: underlayConflict.nameDisplay || underlayConflict.id }) }}
</span>
</div>
</div>
<template v-if="isStaticMode">
<hr class="section-divider" />
<div
v-for="node in nodes"
:key="node.id"
class="row mb-10 ips-row"
>
<div class="col span-3">
<LabeledInput
:value="node.nameDisplay || node.id"
:label="t('harvester.hostNetworkConfig.ips.nodeLabel')"
mode="view"
:disabled="true"
/>
</div>
<div class="col span-5">
<LabeledInput
:value="ips[node.id]"
:label="t('harvester.hostNetworkConfig.ips.label')"
:placeholder="t('harvester.hostNetworkConfig.ips.placeholder')"
:mode="mode"
required
@update:value="updateIp(node.id, $event)"
/>
</div>
</div>
</template>
</Tab>
<Tab
name="nodeSelector"
:label="t('harvester.hostNetworkConfig.tabs.nodeSelector')"
:weight="98"
>
<template v-if="hasNodeSelector">
<InfoBox class="node-selector-box">
<button
v-if="!isView"
type="button"
class="role-link btn btn-sm remove"
:aria-label="t('generic.remove')"
@click="removeNodeSelector"
>
<i class="icon icon-x" />
</button>
<HarvesterNodeSelector
class="mt-20"
:value="value.spec.nodeSelector"
:mode="mode"
/>
</InfoBox>
</template>
<template v-else>
<button
type="button"
class="btn role-secondary"
:disabled="isView"
@click="addNodeSelector"
>
{{ t('harvester.hostNetworkConfig.nodeSelector.addButton') }}
</button>
</template>
</Tab>
</ResourceTabs>
</CruResource>
</template>
<style lang="scss" scoped>
.section-divider {
border: none;
border-top: 1px solid var(--border);
margin: 10px 0 20px;
}
.node-selector-box {
position: relative;
.remove {
position: absolute;
top: 10px;
right: 10px;
z-index: 1;
padding: 0;
}
}
.underlay-conflict-warning {
display: inline-flex;
align-items: center;
gap: 4px;
margin-top: 4px;
color: var(--warning);
font-size: 13px;
}
</style>

View File

@ -31,6 +31,10 @@ export default {
to() { to() {
return this.vm?.detailLocation; return this.vm?.detailLocation;
}, },
attachVMName() {
return this.vm?.nameDisplay || this.vm?.metadata?.name || this.value;
}
} }
}; };
</script> </script>
@ -40,10 +44,10 @@ export default {
v-if="to" v-if="to"
:to="to" :to="to"
> >
{{ value }} {{ attachVMName }}
</router-link> </router-link>
<span v-else> <span v-else>
{{ value }} {{ attachVMName }}
</span> </span>
</template> </template>

View File

@ -0,0 +1,23 @@
<script>
export default {
name: 'HarvesterBooleanFormatter',
props: {
value: {
type: Boolean,
default: false,
},
},
};
</script>
<template>
<span v-if="value">
<i class="icon icon-checkmark" />
</span>
<span
v-else
class="text-muted"
>
&mdash;
</span>
</template>

View File

@ -0,0 +1,29 @@
<script>
export default {
name: 'HarvesterHostNetworkConfigModeFormatter',
props: {
value: {
type: String,
default: '',
},
},
computed: {
displayMode() {
if (this.value?.toLowerCase() === 'dhcp') {
return 'DHCP';
}
if (this.value?.toLowerCase() === 'static') {
return 'Static';
}
return this.value;
},
},
};
</script>
<template>
<span>{{ displayMode }}</span>
</template>

View File

@ -20,12 +20,7 @@ export default {
computed: { computed: {
vmiResource() { vmiResource() {
const vmiList = this.$store.getters['harvester/all'](HCI.VMI) || []; return this.$store.getters['harvester/byId'](HCI.VMI, this.vmResource?.id) || null;
const vmi = vmiList.find( (VMI) => {
return VMI?.metadata?.ownerReferences?.[0]?.uid === this.vmResource?.metadata?.uid;
});
return vmi;
}, },
migrationState() { migrationState() {
return this.vmiResource?.migrationState?.status || ''; return this.vmiResource?.migrationState?.status || '';

View File

@ -0,0 +1,24 @@
<script>
export default {
name: 'HarvesterVlanFormatter',
props: {
value: {
type: String,
default: '',
},
},
};
</script>
<template>
<span v-if="value">
{{ value }}
</span>
<span
v-else
class="text-muted"
>
&mdash;
</span>
</template>

View File

@ -14,20 +14,6 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
allNodeNetwork: {
type: Array,
default: () => {
return [];
}
},
allClusterNetwork: {
type: Array,
default: () => {
return [];
}
}
}, },
data() { data() {

View File

@ -144,6 +144,10 @@ harvester:
migration: migration:
failedMessage: Latest migration failed! failedMessage: Latest migration failed!
title: Migration title: Migration
vmMigrationTitle: '{count, plural, one {Migrating # VM} other {Migrating # VMs}}'
selectedVMs: "The following virtual machine(s) will be migrated to the target node"
unknownNode: (unknown node)
alreadyOnTarget: Already on Target
fields: fields:
nodeName: nodeName:
label: Target Node label: Target Node
@ -248,6 +252,7 @@ harvester:
suspendSchedule: Suspend suspendSchedule: Suspend
restoreExistingVM: Replace Existing restoreExistingVM: Replace Existing
migrate: Migrate migrate: Migrate
vmMigrate: Virtual Machine Migration
cpuAndMemoryHotplug: Edit CPU and Memory cpuAndMemoryHotplug: Edit CPU and Memory
abortMigration: Abort Migration abortMigration: Abort Migration
storageMigration: Storage Migration storageMigration: Storage Migration
@ -290,6 +295,12 @@ harvester:
harvesterIpAddress: harvesterIpAddress:
customIpTooltip: "Custom IP (set via annotation)" customIpTooltip: "Custom IP (set via annotation)"
tableHeaders: tableHeaders:
hostNetworkConfig:
underlay: Underlay
underlayTooltip: Allow this interface to act as the underlay for VM overlay networks.
vlanID: VLAN ID
mode: Mode
clusterNetwork: Cluster Network
imageEncryption: Encryption imageEncryption: Encryption
size: Size size: Size
virtualSize: Virtual Size virtualSize: Virtual Size
@ -298,6 +309,7 @@ harvester:
phase: Phase phase: Phase
attachedVM: Attached Virtual Machine attachedVM: Attached Virtual Machine
cpuManager: CPU Manager cpuManager: CPU Manager
routeConnectivityTooltip: Connectivity between the VM network and the management network, which the Harvester nodes are connected to.
fingerprint: Fingerprint fingerprint: Fingerprint
value: Value value: Value
actions: Actions actions: Actions
@ -336,6 +348,9 @@ harvester:
vmImportSourceOClusterStatus: Cluster Status vmImportSourceOClusterStatus: Cluster Status
vmImportSourceOVAUrl: URL vmImportSourceOVAUrl: URL
vmImportSourceOVAStatus: Status vmImportSourceOVAStatus: Status
v4ip: V4 IP
v6ip: V6 IP
eipName: EIP Name
tab: tab:
volume: Volumes volume: Volumes
network: Networks network: Networks
@ -349,6 +364,7 @@ harvester:
snapshots: Snapshots snapshots: Snapshots
instanceLabel: Instance Labels instanceLabel: Instance Labels
annotations: Annotations annotations: Annotations
filesystem: Filesystem Volume
fields: fields:
version: Version version: Version
name: Name name: Name
@ -385,21 +401,6 @@ harvester:
middle: vGPU Devices middle: vGPU Devices
suffix: page. suffix: page.
deviceInTheSameHost: 'You can only select devices on the same host.' deviceInTheSameHost: 'You can only select devices on the same host.'
oldFormatDevices:
help: |-
<p>
The following PCI devices are using the old naming convention and need to be updated in the YAML file:
</p>
<ul>
{oldFormatDevicesHTML}
</ul>
<p>
Please use the following instructions to update the virtual machine:
</p>
<ol>
<li>Stop the virtual machine, edit the virtual machine YAML, and remove the <Code>hostDevices</Code> section, and save virtual machine the changes to the YAML file.</li>
<li>Edit the virtual machine, and add the already enabled PCI Device from the list of available PCIDevices, and save and start VM.</li>
</ol>
showCompatibility: Show device compatibility matrix showCompatibility: Show device compatibility matrix
hideCompatibility: Hide device compatibility matrix hideCompatibility: Hide device compatibility matrix
claimError: Error enabling passthrough on {name} claimError: Error enabling passthrough on {name}
@ -417,6 +418,8 @@ harvester:
suffix: to enable the add-on to successfully manage your PCI devices. suffix: to enable the add-on to successfully manage your PCI devices.
noPCIPermission: Please contact your system administrator to enable the PCI devices first. noPCIPermission: Please contact your system administrator to enable the PCI devices first.
enablePassthroughWarning: Please be careful not to use host-owned PCI devices (e.g., management and VLAN NICs). Incorrect device allocation may cause damage to your cluster, including node failure. enablePassthroughWarning: Please be careful not to use host-owned PCI devices (e.g., management and VLAN NICs). Incorrect device allocation may cause damage to your cluster, including node failure.
disableResourcePooling: Disable Resource Pooling
disableResourcePoolingDescription: Assigns this device a unique resource name so it can be pinned to a specific VM, instead of sharing the pool with other identical devices.
devices: devices:
matrixHostName: Host Name matrixHostName: Host Name
@ -726,6 +729,7 @@ harvester:
other {Start} other {Start}
} Now } Now
createSSHKey: Create a New... createSSHKey: Create a New...
genericLoginError: Authentication failed. Please re-log in and try again.
installAgent: Install guest agent installAgent: Install guest agent
enableUsb: Enable USB Tablet enableUsb: Enable USB Tablet
advancedOptions: advancedOptions:
@ -780,7 +784,7 @@ harvester:
addPort: Add Port addPort: Add Port
cloudConfig: cloudConfig:
title: Cloud Configuration title: Cloud Configuration
createTemplateTitle: 'Create {name}.' createTemplateTitle: 'Create {name}'
createNew: Create new... createNew: Create new...
cloudInit: cloudInit:
label: Cloud Init label: Cloud Init
@ -823,6 +827,18 @@ harvester:
username: Username username: Username
password: Password password: Password
reservedMemory: Reserved Memory reservedMemory: Reserved Memory
customizeDisplayName: Customize virtual machine display name
displayNamePlaceholder: Virtual machine alias name
displayName: Display Name
filesystem:
description: Harvester supports filesystem volumes for VM via virtiofs.
type: Filesystem Type
volume: Volume
resource: Resource
add: Add
mountBannerHint: "Please update the mount path (e.g. /mnt/appconfigfs) to your preferred location, then add the corresponding commands to the"
mountBannerHintLink: "runcmd"
mountBannerHintSuffix: "in Advanced tab User Data."
machineTypeTip: 'Specify a processor architecture to emulate. To see a list of supported architectures, run: qemu-system-x86_64 -cpu ?' machineTypeTip: 'Specify a processor architecture to emulate. To see a list of supported architectures, run: qemu-system-x86_64 -cpu ?'
detail: detail:
tabs: tabs:
@ -1111,6 +1127,7 @@ harvester:
replaceExisting: Replace existing replaceExisting: Replace existing
virtualMachineName: Virtual Machine Name virtualMachineName: Virtual Machine Name
keepMacAddress: Keep MAC Address keepMacAddress: Keep MAC Address
haltAfterRestore: Keep powered off after restore
matchTarget: The current backup target does not match the existing one. matchTarget: The current backup target does not match the existing one.
progress: progress:
details: Volume details details: Volume details
@ -1141,6 +1158,9 @@ harvester:
gateway: gateway:
label: Gateway IP label: Gateway IP
placeholder: e.g. 172.20.0.1 placeholder: e.g. 172.20.0.1
vlan:
label: VLAN
placeholder: Select a VLAN
dhcp: dhcp:
label: Dynamic Host Configuration Protocol (DHCP) label: Dynamic Host Configuration Protocol (DHCP)
v4Options: DHCPV4Options v4Options: DHCPV4Options
@ -1148,6 +1168,9 @@ harvester:
placeholder: key1=value1, key2=value2 placeholder: key1=value1, key2=value2
dhcpOptionBanner: DHCP options is a key/value string concatenate with comma. For more detail, please refer to <a href="https://kubeovn.github.io/docs/v1.13.x/en/kubevirt/dhcp/" target="_blank">KubOVN document</a> dhcpOptionBanner: DHCP options is a key/value string concatenate with comma. For more detail, please refer to <a href="https://kubeovn.github.io/docs/v1.13.x/en/kubevirt/dhcp/" target="_blank">KubOVN document</a>
tooltip: Enable DHCP server for this subnet. When enabled, VMs can automatically obtain IP addresses from this subnet. tooltip: Enable DHCP server for this subnet. When enabled, VMs can automatically obtain IP addresses from this subnet.
externalConnectivity:
label: NAT Outgoing
tooltip: Enable NAT for VMs using this subnet to access external networks.
private: private:
label: Private Subnet 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. 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.
@ -1223,14 +1246,97 @@ harvester:
remoteVpc: remoteVpc:
label: Remote VPC label: Remote VPC
infoBanner: The static route destination CIDR must cover all subnets CIDR from remote VPC Peer. Read <a href="{url}" target="_blank">VPC Peering Configuration Examples</a> for more information. infoBanner: The static route destination CIDR must cover all subnets CIDR from remote VPC Peer. Read <a href="{url}" target="_blank">VPC Peering Configuration Examples</a> for more information.
natGateway:
label: Gateways
internalTenantNetwork:
label: Internal Tenant Network
placeholder: Select a Virtual Machine Network
vpc:
label: VPC
placeholder: Select a VPC
subnet:
label: Subnet
placeholder: Select a subnet
lanIp:
label: LAN IP
placeholder: Enter LAN IP
externalSubnets:
label: External Subnets
addLabel: Add
placeholder: Select a subnet
externalIP:
label: External IPs
natGateway:
label: VpcNatGateway
placeholder: Select a VpcNatGateway
externalSubnet:
label: External Subnet
placeholder: Select an external subnet
v4ip:
label: V4 IP
placeholder: public ip from external subnet
snat:
label: Source Rules
eip:
label: EIP
placeholder: Select an external IP
internalCIDR:
label: Internal CIDR
placeholder: internal subnet CIDR
dnat:
label: Destination Rules
eip:
label: EIP
placeholder: Select an external IP
externalPort:
label: External Port
placeholder: port number
internalIp:
label: Internal IP
placeholder: internal IP address
internalPort:
label: Internal Port
placeholder: port number
protocol:
label: Protocol
placeholder: Select protocol (tcp or udp)
providerNetwork:
label: Provider Networks
defaultInterface:
label: Default Interface
placeholder: Select the interface the same as master interface of external overlay network
customInterfaces:
label: Custom Interfaces
addLabel: Add Custom Interface
interface:
label: Network Interface
placeholder: e.g. eth2
nodes:
label: Nodes
addLabel: Add Node
placeholder: Select a node
excludedNodes:
label: Excluded Nodes
addLabel: Add Excluded Node
placeholder: Select node to exclude from this provider network
vlanNetwork:
label: VLANs
vlan:
id:
label: VLAN ID
placeholder: "e.g. 1-4094"
provider:
label: Provider Network
placeholder: Select a provider network
networkPolicy: networkPolicy:
label: Network Policies label: Policies
banner: The network policies must be used for VMs attached to overlay networks. Please read the <a href="{url}" target="_blank">harvester document</a> how the network policy works. banner: The network policies must be used for VMs attached to overlay networks. Please read the <a href="{url}" target="_blank">harvester document</a> how the network policy works.
network: network:
label: Virtual Machine Networks label: Virtual Machine Networks
tabs: tabs:
basics: Basics basics: Basics
layer3Network: Route layer3Network: Route
nic: Network Interface Card
clusterNetwork: clusterNetwork:
label: Cluster Network label: Cluster Network
create: Create a new cluster network create: Create a new cluster network
@ -1285,6 +1391,13 @@ harvester:
rancherCluster: rancherCluster:
kubeConfig: Rancher KubeConfig kubeConfig: Rancher KubeConfig
removeUpstreamClusterWhenNamespaceIsDeleted: Remove Upstream Cluster When Namespace Is Deleted removeUpstreamClusterWhenNamespaceIsDeleted: Remove Upstream Cluster When Namespace Is Deleted
clusterPodSecurityStandard:
whitelistedNamespaces:
label: 'Whitelisted Namespaces'
privilegedNamespaces:
label: 'Privileged Namespaces'
restrictedNamespaces:
label: 'Restricted Namespaces'
storageNetwork: storageNetwork:
range: range:
placeholder: e.g. 172.16.0.0/24 placeholder: e.g. 172.16.0.0/24
@ -1292,6 +1405,7 @@ harvester:
invalid: '"Range" is invalid.' invalid: '"Range" is invalid.'
clusterNetwork: Cluster Network clusterNetwork: Cluster Network
vlan: VLAN ID vlan: VLAN ID
exclusiveVlan: Exclusive VLAN
exclude: exclude:
label: Exclude IPs label: Exclude IPs
placeholder: CIDR format, e.g. 172.16.0.10/32 placeholder: CIDR format, e.g. 172.16.0.10/32
@ -1508,6 +1622,7 @@ harvester:
vmSnapshot: vmSnapshot:
label: Virtual Machine Snapshots label: Virtual Machine Snapshots
createText: Restore Snapshot createText: Restore Snapshot
title: Restore Virtual Machine
snapshot: Snapshot snapshot: Snapshot
storage: storage:
@ -1515,6 +1630,7 @@ harvester:
useDefault: Use the default storage useDefault: Use the default storage
volumeEncryption: Volume Encryption volumeEncryption: Volume Encryption
secret: Secret secret: Secret
volumeExpansionCheckbox: Enable Expansion
migratable: migratable:
label: Migratable label: Migratable
numberOfReplicas: numberOfReplicas:
@ -1607,8 +1723,9 @@ harvester:
schedulingRules: Select node(s) matching rules schedulingRules: Select node(s) matching rules
uplink: uplink:
nics: nics:
label: NICs label: NIC
addLabel: Add NIC addLabel: Add NIC
overlayWarning: The NIC selected here must match the NIC provided in the provider network.
placeholder: Select a NIC that is available on all the selected nodes placeholder: Select a NIC that is available on all the selected nodes
validate: validate:
available: NIC "{nic}" is not available on the selected nodes available: NIC "{nic}" is not available on the selected nodes
@ -1764,6 +1881,8 @@ harvester:
repository: Image Repository repository: Image Repository
driver: driver:
location: Driver Location location: Driver Location
enable:
title: Enable NVIDIA Driver Toolkit
parsingSpecError: parsingSpecError:
The field 'spec.valuesContent' has invalid format. The field 'spec.valuesContent' has invalid format.
usbController: usbController:
@ -1848,6 +1967,32 @@ harvester:
addLabel: Add CIDR addLabel: Add CIDR
range: range:
addLabel: Add Range addLabel: Add Range
hostNetworkConfig:
label: Host Networks
mode:
label: Mode
tabs:
mode: Mode
nodeSelector: Node Selector
nodeSelector:
addButton: Add Node Selector
underlay:
label: Underlay
tooltip: Allow this interface to act as the underlay for VM overlay networks.
conflict: '`{name}` host network config already has underlay enabled. Only one underlay is allowed in the cluster.'
noKubeovn:
prefix: The kubeovn-operator add-on is not enabled. Click
middle: here
suffix: to enable the add-on for overlay networking.
vlanID:
label: VLAN ID
ipRange:
label: IP Range ({node})
placeholder: e.g. 192.168.1.10/24
ips:
nodeLabel: Node
label: IP
placeholder: 'e.g. 192.168.1.10/24'
service: service:
healthCheckPort: healthCheckPort:
@ -1893,6 +2038,8 @@ harvester:
migconfiguration: migconfiguration:
label: vGPU MIG Configurations label: vGPU MIG Configurations
status:
outOfSync: Out of Sync
infoBanner: To configure the MIG configuration, please disable it first and re-enable after editing the configuration. infoBanner: To configure the MIG configuration, please disable it first and re-enable after editing the configuration.
profileSpec: Profile Specs profileSpec: Profile Specs
profileStatus: Profile Status profileStatus: Profile Status
@ -1955,6 +2102,7 @@ harvester:
title: Cannot Disable Passthrough title: Cannot Disable Passthrough
message: Please detach the device from the VM and save it first before disabling passthrough. message: Please detach the device from the VM and save it first before disabling passthrough.
enablePassthroughWarning: 'Please re-enable the USB device if the device path changes in the following situations:<br/>&nbsp1) Re-plugging the USB device.<br/>&nbsp2) Rebooting the node.<br/><br/>An incorrect device path may cause passthrough to fail.' enablePassthroughWarning: 'Please re-enable the USB device if the device path changes in the following situations:<br/>&nbsp1) Re-plugging the USB device.<br/>&nbsp2) Rebooting the node.<br/><br/>An incorrect device path may cause passthrough to fail.'
classType: Class Type
harvesterVlanConfigMigrateDialog: harvesterVlanConfigMigrateDialog:
targetClusterNetwork: targetClusterNetwork:
@ -2032,6 +2180,8 @@ advancedSettings:
'harv-auto-rotate-rke2-certs': The certificate rotation mechanism relies on Rancher. Harvester will automatically update certificates generation to trigger rotation. 'harv-auto-rotate-rke2-certs': The certificate rotation mechanism relies on Rancher. Harvester will automatically update certificates generation to trigger rotation.
'harv-kubeconfig-default-token-ttl-minutes': 'TTL (in minutes) applied on Harvester administration kubeconfig files. Default is 0, which means to never expire.' '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-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-longhorn-v2-data-engine-hugepage-enabled': 'Enable hugepages when using the Longhorn V2 data engine. Default is true. Disabling hugepages reduces memory pressure on low-spec nodes and increases deployment flexibility. However, performance may be lower compared to running with hugepages.'
'harv-longhorn-v2-data-engine-memory-size': 'Configure the amount of memory allocated to the SPDK target daemon when using the Longhorn V2 data engine. Default is 2048 MiB.'
'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-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-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-vm-migration-network': 'Segregated network for VM migration traffic.'
@ -2039,6 +2189,7 @@ advancedSettings:
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.' 'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
'harv-kubevirt-migration': 'Configure cluster-wide KubeVirt live migration parameters.' 'harv-kubevirt-migration': 'Configure cluster-wide KubeVirt live migration parameters.'
'harv-instance-manager-resources': 'Configure resource percentage reservations for Longhorn instance manager V1 and V2. Valid instance manager CPU range between 0 - 40.' 'harv-instance-manager-resources': 'Configure resource percentage reservations for Longhorn instance manager V1 and V2. Valid instance manager CPU range between 0 - 40.'
'harv-cluster-pod-security-standard': 'Enforce Kubernetes Pod Security Standards (PSS) at the cluster level.'
typeLabel: typeLabel:
kubevirt.io.virtualmachine: |- kubevirt.io.virtualmachine: |-
@ -2101,6 +2252,36 @@ typeLabel:
one { Virtual Private Cloud } one { Virtual Private Cloud }
other { Virtual Private Clouds } other { Virtual Private Clouds }
} }
kubeovn.io.vlan: |-
{count, plural,
one { VLAN Network }
other { VLAN Networks }
}
kubeovn.io.providernetwork: |-
{count, plural,
one { Provider Network }
other { Provider Networks }
}
kubeovn.io.vpcnatgateway: |-
{count, plural,
one { NAT Gateway }
other { NAT Gateways }
}
kubeovn.io.iptablessnatrule: |-
{count, plural,
one { Source Rule }
other { Source Rules }
}
kubeovn.io.iptablesdnatrule: |-
{count, plural,
one { Destination Rule }
other { Destination Rules }
}
kubeovn.io.iptableseip: |-
{count, plural,
one { External IP }
other { External IPs }
}
networking.k8s.io.networkpolicy: |- networking.k8s.io.networkpolicy: |-
{count, plural, {count, plural,
one { Network Policy } one { Network Policy }
@ -2161,16 +2342,17 @@ typeLabel:
one { PCI Device } one { PCI Device }
other { PCI Devices } other { PCI Devices }
} }
persistentvolumeclaim: |-
{count, plural,
one { Volume }
other { Volumes }
}
network.harvesterhci.io.clusternetwork: |- network.harvesterhci.io.clusternetwork: |-
{count, plural, {count, plural,
one { Cluster Network } one { Cluster Network }
other { Cluster Networks } other { Cluster Networks }
} }
network.harvesterhci.io.hostnetworkconfig: |-
{count, plural,
one { Host Network }
other { Host Networks }
}
harvesterhci.io.addon: |- harvesterhci.io.addon: |-
{count, plural, {count, plural,
one { Add-on } one { Add-on }

View File

@ -3,7 +3,6 @@ import { Banner } from '@components/Banner';
import Loading from '@shell/components/Loading'; import Loading from '@shell/components/Loading';
import ResourceTable from '@shell/components/ResourceTable'; import ResourceTable from '@shell/components/ResourceTable';
import BadgeState from '@shell/components/formatter/BadgeStateFormatter'; import BadgeState from '@shell/components/formatter/BadgeStateFormatter';
import { NAME, AGE, NAMESPACE, STATE } from '@shell/config/table-headers'; import { NAME, AGE, NAMESPACE, STATE } from '@shell/config/table-headers';
import { NETWORK_ATTACHMENT, SCHEMA } from '@shell/config/types'; import { NETWORK_ATTACHMENT, SCHEMA } from '@shell/config/types';
import { allHash } from '@shell/utils/promise'; import { allHash } from '@shell/utils/promise';
@ -113,6 +112,7 @@ export default {
value: 'connectivity', value: 'connectivity',
labelKey: 'tableHeaders.routeConnectivity', labelKey: 'tableHeaders.routeConnectivity',
formatter: 'NetworkRouteConnectivity', formatter: 'NetworkRouteConnectivity',
tooltip: 'harvester.tableHeaders.routeConnectivityTooltip',
formatterOpts: { arbitrary: true }, formatterOpts: { arbitrary: true },
width: 130, width: 130,
}, },
@ -166,6 +166,7 @@ export default {
:schema="schema" :schema="schema"
:groupable="true" :groupable="true"
:rows="filterRows" :rows="filterRows"
:ignore-filter="true"
key-field="_key" key-field="_key"
> >
<template #cell:state="{row}"> <template #cell:state="{row}">

View File

@ -106,8 +106,8 @@ export default {
name: 'AttachedVM', name: 'AttachedVM',
labelKey: 'tableHeaders.attachedVM', labelKey: 'tableHeaders.attachedVM',
type: 'attached', type: 'attached',
value: 'spec.claimRef', value: 'attachVMName',
sort: 'name', sort: 'attachVMName',
}, },
{ {
name: 'VolumeSnapshotCounts', name: 'VolumeSnapshotCounts',
@ -134,8 +134,8 @@ export default {
return row?.attachVM?.detailLocation; return row?.attachVM?.detailLocation;
}, },
getVMName(row) { getAttachedVMName(row) {
return row.attachVM?.metadata?.name || ''; return row.attachVMName || '';
}, },
isInternalStorageClass(storageClassName) { isInternalStorageClass(storageClassName) {
@ -173,10 +173,10 @@ export default {
<template #cell:AttachedVM="{row}"> <template #cell:AttachedVM="{row}">
<div> <div>
<router-link <router-link
v-if="getVMName(row)" v-if="getAttachedVMName(row)"
:to="goTo(row)" :to="goTo(row)"
> >
{{ getVMName(row) }} {{ getAttachedVMName(row) }}
</router-link> </router-link>
</div> </div>
</template> </template>

View File

@ -67,6 +67,13 @@ export default {
NAMESPACE, NAMESPACE,
CIDR_BLOCK, CIDR_BLOCK,
PROTOCOL, PROTOCOL,
{
name: 'vlan',
labelKey: 'harvester.subnet.vlan.label',
value: 'spec.vlan',
sort: 'spec.vlan',
formatter: 'HarvesterVlan',
},
PROVIDER, PROVIDER,
AGE AGE
]; ];

View File

@ -1,4 +1,5 @@
<script> <script>
import { mapGetters } from 'vuex';
import ResourceTable from '@shell/components/ResourceTable'; import ResourceTable from '@shell/components/ResourceTable';
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers'; import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
import { import {
@ -12,11 +13,18 @@ import { HCI } from '../types';
import HarvesterVmState from '../formatters/HarvesterVmState'; import HarvesterVmState from '../formatters/HarvesterVmState';
import ConsoleBar from '../components/VMConsoleBar'; import ConsoleBar from '../components/VMConsoleBar';
const ENCRYPTED_VOLUME_TOOLTIP_KEYS = {
all: 'harvester.virtualMachine.volume.lockTooltip.all',
partial: 'harvester.virtualMachine.volume.lockTooltip.partial',
};
export const VM_HEADERS = [ export const VM_HEADERS = [
STATE, STATE,
{ {
...NAME, ...NAME,
width: 350, width: 350,
value: 'nameDisplay',
sort: ['nameDisplay'],
}, },
NAMESPACE, NAMESPACE,
{ {
@ -93,19 +101,9 @@ export default {
this.hasNode = true; this.hasNode = true;
} }
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.NODE_NETWORK)) {
_hash.nodeNetworks = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.NODE_NETWORK });
}
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.CLUSTER_NETWORK)) {
_hash.clusterNetworks = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK });
}
const hash = await allHash(_hash); const hash = await allHash(_hash);
this.allVMs = hash.vms; this.allVMs = hash.vms;
this.allNodeNetworks = hash.nodeNetworks || [];
this.allClusterNetworks = hash.clusterNetworks || [];
}, },
data() { data() {
@ -113,14 +111,14 @@ export default {
hasNode: false, hasNode: false,
allVMs: [], allVMs: [],
allVMIs: [], allVMIs: [],
allNodeNetworks: [],
allClusterNetworks: [],
restartNotificationDisplayed: false, restartNotificationDisplayed: false,
HCI HCI
}; };
}, },
computed: { computed: {
...mapGetters({ actionCb: 'action-menu/performCallbackData' }),
headers() { headers() {
const restoreCol = { const restoreCol = {
name: 'restoreProgress', name: 'restoreProgress',
@ -163,6 +161,12 @@ export default {
*/ */
hasBackUpRestoreInProgress() { hasBackUpRestoreInProgress() {
return !!this.rows.find((r) => r.restoreResource && !r.restoreResource.fromSnapshot && !r.restoreResource.isComplete); return !!this.rows.find((r) => r.restoreResource && !r.restoreResource.fromSnapshot && !r.restoreResource.isComplete);
},
vmRestartRequiredNames() {
return this.allVMs
.filter((vm) => vm.isRestartRequired)
.map((vm) => vm.metadata.name);
} }
}, },
@ -181,18 +185,17 @@ export default {
}, },
watch: { watch: {
allVMs: { actionCb(neu) {
handler(neu) { if (neu?.clearTableSelection) {
const vmNames = []; this.$refs.resourceTable.clearSelection();
this.$store.dispatch('action-menu/clearCallbackData');
neu.forEach((vm) => {
if (vm.isRestartRequired) {
vmNames.push(vm.metadata.name);
} }
}); },
vmRestartRequiredNames(vmNames) {
const count = vmNames.length; const count = vmNames.length;
if ( count === 0 && this.restartNotificationDisplayed) { if (count === 0 && this.restartNotificationDisplayed) {
this.restartNotificationDisplayed = false; this.restartNotificationDisplayed = false;
return; return;
@ -203,9 +206,7 @@ export default {
if (this.restartNotificationDisplayed) { if (this.restartNotificationDisplayed) {
this.$store.dispatch('growl/clear'); this.$store.dispatch('growl/clear');
} }
}
if (count > 0 && vmNames.length > 0) {
this.$store.dispatch('growl/warning', { this.$store.dispatch('growl/warning', {
title: this.t('harvester.notification.restartRequired.title', { count }), title: this.t('harvester.notification.restartRequired.title', { count }),
message: this.t('harvester.notification.restartRequired.message', { vmNames: vmNames.join(', ') }), message: this.t('harvester.notification.restartRequired.message', { vmNames: vmNames.join(', ') }),
@ -213,21 +214,13 @@ export default {
}, { root: true }); }, { root: true });
this.restartNotificationDisplayed = true; this.restartNotificationDisplayed = true;
} }
},
deep: true,
} }
}, },
methods: { methods: {
lockIconTooltipMessage(row) { lockIconTooltipMessage(row) {
const message = ''; const key = ENCRYPTED_VOLUME_TOOLTIP_KEYS[row.encryptedVolumeType];
if (row.encryptedVolumeType === 'all') { return key ? this.t(key) : '';
return this.t('harvester.virtualMachine.volume.lockTooltip.all');
} else if (row.encryptedVolumeType === 'partial') {
return this.t('harvester.virtualMachine.volume.lockTooltip.partial');
}
return message;
} }
} }
}; };
@ -237,6 +230,7 @@ export default {
<Loading v-if="$fetchState.pending" /> <Loading v-if="$fetchState.pending" />
<div v-else> <div v-else>
<ResourceTable <ResourceTable
ref="resourceTable"
v-bind="$attrs" v-bind="$attrs"
:headers="headers" :headers="headers"
default-sort-by="age" default-sort-by="age"
@ -253,8 +247,6 @@ export default {
<HarvesterVmState <HarvesterVmState
class="vmstate" class="vmstate"
:row="scope.row" :row="scope.row"
:all-node-network="allNodeNetworks"
:all-cluster-network="allClusterNetworks"
/> />
</div> </div>
</template> </template>
@ -265,16 +257,16 @@ export default {
v-if="scope.row.type !== HCI.VMI" v-if="scope.row.type !== HCI.VMI"
:to="scope.row.detailLocation" :to="scope.row.detailLocation"
> >
{{ scope.row.metadata.name }} {{ scope.row.nameDisplay }}
<i <i
v-if="lockIconTooltipMessage(scope.row)" v-if="scope.row.encryptedVolumeType !== 'none'"
v-tooltip="lockIconTooltipMessage(scope.row)" v-tooltip="lockIconTooltipMessage(scope.row)"
class="icon icon-lock" class="icon icon-lock"
:class="{'green-icon': scope.row.encryptedVolumeType === 'all', 'yellow-icon': scope.row.encryptedVolumeType === 'partial'}" :class="{'green-icon': scope.row.encryptedVolumeType === 'all', 'yellow-icon': scope.row.encryptedVolumeType === 'partial'}"
/> />
</router-link> </router-link>
<span v-else> <span v-else>
{{ scope.row.metadata.name }} {{ scope.row.nameDisplay }}
</span> </span>
<ConsoleBar <ConsoleBar
:resource-type="scope.row" :resource-type="scope.row"

View File

@ -0,0 +1,86 @@
<script>
import ResourceTable from '@shell/components/ResourceTable';
import Loading from '@shell/components/Loading';
import { STATE, NAME as NAME_COL, AGE } from '@shell/config/table-headers';
import { HCI } from '../types';
const UNDERLAY = {
name: 'underlay',
labelKey: 'harvester.tableHeaders.hostNetworkConfig.underlay',
tooltip: 'harvester.tableHeaders.hostNetworkConfig.underlayTooltip',
value: 'spec.underlay',
sort: 'spec.underlay',
formatter: 'HarvesterBoolean',
};
const VLAN_ID = {
name: 'vlanID',
labelKey: 'harvester.tableHeaders.hostNetworkConfig.vlanID',
value: 'spec.vlanID',
sort: 'spec.vlanID',
};
const MODE = {
name: 'mode',
labelKey: 'harvester.tableHeaders.hostNetworkConfig.mode',
value: 'spec.mode',
sort: 'spec.mode',
formatter: 'HarvesterHostNetworkConfigMode',
};
const CLUSTER_NETWORK = {
name: 'clusterNetwork',
labelKey: 'harvester.tableHeaders.hostNetworkConfig.clusterNetwork',
value: 'spec.clusterNetwork',
sort: 'spec.clusterNetwork',
align: 'center',
};
export default {
name: 'HarvesterListHostNetworkConfig',
components: { ResourceTable, Loading },
inheritAttrs: false,
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HOST_NETWORK_CONFIG });
},
data() {
return { rows: [] };
},
computed: {
schema() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/schemaFor`](HCI.HOST_NETWORK_CONFIG);
},
headers() {
return [
STATE,
NAME_COL,
UNDERLAY,
VLAN_ID,
MODE,
CLUSTER_NETWORK,
AGE,
];
},
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<ResourceTable
v-else
v-bind="$attrs"
:headers="headers"
:rows="rows"
:schema="schema"
key-field="_key"
/>
</template>

View File

@ -12,7 +12,7 @@ import { base64Decode } from '@shell/utils/crypto';
import { formatSi, parseSi } from '@shell/utils/units'; import { formatSi, parseSi } from '@shell/utils/units';
import { _CLONE, _CREATE, _VIEW } from '@shell/config/query-params'; import { _CLONE, _CREATE, _VIEW } from '@shell/config/query-params';
import { import {
PV, PVC, STORAGE_CLASS, NODE, SECRET, CONFIG_MAP, NETWORK_ATTACHMENT, NAMESPACE, LONGHORN PV, PVC, STORAGE_CLASS, NODE, SECRET, CONFIG_MAP, SERVICE_ACCOUNT, NETWORK_ATTACHMENT, NAMESPACE, LONGHORN
} from '@shell/config/types'; } from '@shell/config/types';
import { HOSTNAME } from '@shell/config/labels-annotations'; import { HOSTNAME } from '@shell/config/labels-annotations';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
@ -25,7 +25,7 @@ import { HCI } from '../../types';
import { parseVolumeClaimTemplates, EMPTY_IMAGE } from '../../utils/vm'; import { parseVolumeClaimTemplates, EMPTY_IMAGE } from '../../utils/vm';
import impl, { QGA_JSON, USB_TABLET } from './impl'; import impl, { QGA_JSON, USB_TABLET } from './impl';
import { GIBIBYTE } from '../../utils/unit'; import { GIBIBYTE } from '../../utils/unit';
import { VOLUME_MODE } from '@pkg/harvester/config/types'; import { VOLUME_MODE, FILESYSTEM_SOURCE_TYPE } from '@pkg/harvester/config/types';
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine'; const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
@ -102,6 +102,8 @@ export default {
vmims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIM }), vmims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIM }),
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }), vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
secrets: this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }), secrets: this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }),
configMaps: this.$store.dispatch(`${ inStore }/findAll`, { type: CONFIG_MAP }),
serviceAccounts: this.$store.dispatch(`${ inStore }/findAll`, { type: SERVICE_ACCOUNT }),
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }), addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
longhornV2Engine: this.$store.dispatch(`${ inStore }/find`, { type: LONGHORN.SETTINGS, id: LONGHORN_V2_DATA_ENGINE }), longhornV2Engine: this.$store.dispatch(`${ inStore }/find`, { type: LONGHORN.SETTINGS, id: LONGHORN_V2_DATA_ENGINE }),
}; };
@ -156,6 +158,7 @@ export default {
imageId: '', imageId: '',
diskRows: [], diskRows: [],
networkRows: [], networkRows: [],
filesystemRows: [],
machineType: '', machineType: '',
machineTypes: [], machineTypes: [],
secretName: '', secretName: '',
@ -440,6 +443,7 @@ export default {
this['imageId'] = imageId; this['imageId'] = imageId;
this['diskRows'] = diskRows; this['diskRows'] = diskRows;
this['filesystemRows'] = this.getFilesystemRows(vm);
this.refreshYamlEditor(); this.refreshYamlEditor();
}, },
@ -641,6 +645,80 @@ export default {
this.parseAccessCredentials(); this.parseAccessCredentials();
this.parseNetworkRows(this.networkRows); this.parseNetworkRows(this.networkRows);
this.parseDiskRows(this.diskRows); this.parseDiskRows(this.diskRows);
this.parseFilesystemRows();
},
getFilesystemRows(vm) {
const _filesystems = vm.spec.template.spec.domain.devices?.filesystems || [];
const _volumes = vm.spec.template.spec.volumes || [];
return _filesystems.map((fs) => {
const volume = _volumes.find((v) => v.name === fs.name);
let fsType = FILESYSTEM_SOURCE_TYPE.CONFIGMAP;
let resourceName = '';
if (volume?.configMap) {
fsType = FILESYSTEM_SOURCE_TYPE.CONFIGMAP;
resourceName = volume.configMap.name;
} else if (volume?.secret) {
fsType = FILESYSTEM_SOURCE_TYPE.SECRET;
resourceName = volume.secret.secretName;
} else if (volume?.serviceAccount) {
fsType = FILESYSTEM_SOURCE_TYPE.SERVICEACCOUNT;
resourceName = volume.serviceAccount.serviceAccountName;
}
return {
fsType,
volumeName: fs.name,
resourceName,
};
});
},
parseFilesystemRows() {
const completedRows = this.filesystemRows.filter(
(r) => r.fsType && r.volumeName && r.resourceName
);
const filesystems = completedRows.map((r) => ({
name: r.volumeName,
virtiofs: {},
}));
const fsVolumes = completedRows.map((r) => {
if (r.fsType === FILESYSTEM_SOURCE_TYPE.CONFIGMAP) {
return {
name: r.volumeName,
configMap: { name: r.resourceName },
};
} else if (r.fsType === FILESYSTEM_SOURCE_TYPE.SECRET) {
return {
name: r.volumeName,
secret: { secretName: r.resourceName },
};
} else if (r.fsType === FILESYSTEM_SOURCE_TYPE.SERVICEACCOUNT) {
return {
name: r.volumeName,
serviceAccount: { serviceAccountName: r.resourceName },
};
}
return null;
}).filter(Boolean);
if (filesystems.length > 0) {
this.spec.template.spec.domain.devices['filesystems'] = filesystems;
} else {
delete this.spec.template.spec.domain.devices['filesystems'];
}
if (fsVolumes.length > 0) {
if (!this.spec.template.spec.volumes) {
this.spec.template.spec['volumes'] = [];
}
this.spec.template.spec.volumes.push(...fsVolumes);
}
}, },
parseOther() { parseOther() {
@ -907,14 +985,22 @@ export default {
const specInterfaces = this.spec?.template?.spec?.domain?.devices?.interfaces; const specInterfaces = this.spec?.template?.spec?.domain?.devices?.interfaces;
const mergedInterfaces = this.mergeInterfaceList(specInterfaces, interfaces); const mergedInterfaces = this.mergeInterfaceList(specInterfaces, interfaces);
const devices = {
...this.spec.template.spec.domain.devices,
interfaces: mergedInterfaces,
};
if (this.isEdit && networkRow.length === 0) {
devices.autoattachPodInterface = false;
} else {
delete devices.autoattachPodInterface;
}
const spec = { const spec = {
...this.spec.template.spec, ...this.spec.template.spec,
domain: { domain: {
...this.spec.template.spec.domain, ...this.spec.template.spec.domain,
devices: { devices,
...this.spec.template.spec.domain.devices,
interfaces: mergedInterfaces,
},
}, },
networks networks
}; };
@ -1656,7 +1742,7 @@ export default {
const oldImageId = old[0]?.image; const oldImageId = old[0]?.image;
if (this.isCreate && oldImageId === imageId && imageId) { if (this.isCreate && oldImageId !== imageId && imageId && osType) {
this.osType = osType; this.osType = osType;
} }
} }

View File

@ -9,6 +9,7 @@ import HarvesterResource from './harvester';
export default class MIGCONFIGURATION extends HarvesterResource { export default class MIGCONFIGURATION extends HarvesterResource {
get _availableActions() { get _availableActions() {
let out = super._availableActions; let out = super._availableActions;
const canUpdate = !!this.linkFor('update');
out = out.map((action) => { out = out.map((action) => {
if (action.action === 'showConfiguration') { if (action.action === 'showConfiguration') {
@ -26,13 +27,13 @@ export default class MIGCONFIGURATION extends HarvesterResource {
out.push( out.push(
{ {
action: 'enableConfig', action: 'enableConfig',
enabled: !this.isEnabled, enabled: !this.isEnabled && canUpdate,
icon: 'icon icon-fw icon-dot', icon: 'icon icon-fw icon-dot',
label: 'Enable', label: 'Enable',
}, },
{ {
action: 'disableConfig', action: 'disableConfig',
enabled: this.isEnabled, enabled: this.isEnabled && canUpdate,
icon: 'icon icon-fw icon-dot-open', icon: 'icon icon-fw icon-dot-open',
label: 'Disable', label: 'Disable',
}, },
@ -62,13 +63,27 @@ export default class MIGCONFIGURATION extends HarvesterResource {
} }
get stateDisplay() { get stateDisplay() {
if (this.configStatus === 'out-of-sync') {
return this.t('harvester.migconfiguration.status.outOfSync');
}
return this.actualState; return this.actualState;
} }
get stateColor() { get stateDescription() {
const state = this.actualState; if (this.status?.message) {
return this.status.message;
}
return colorForState(state); return super.stateDescription;
}
get stateColor() {
if (this.configStatus === 'out-of-sync') {
return 'text-warning';
}
return colorForState(this.actualState);
} }
get isEnabled() { get isEnabled() {

View File

@ -34,7 +34,7 @@ export default class PCIDevice extends SteveModel {
out.push( out.push(
{ {
action: 'enablePassthroughBulk', action: 'enablePassthroughBulk',
enabled: !this.isEnabling && !this.isvGPUDevice, enabled: !this.isEnabling && !this.isvGPUDevice && this.canUpdate,
icon: 'icon icon-fw icon-dot', icon: 'icon icon-fw icon-dot',
label: 'Enable Passthrough', label: 'Enable Passthrough',
bulkable: true, bulkable: true,
@ -43,7 +43,7 @@ export default class PCIDevice extends SteveModel {
}, },
{ {
action: 'disablePassthrough', action: 'disablePassthrough',
enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice, enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice && this.canUpdate,
icon: 'icon icon-fw icon-dot-open', icon: 'icon icon-fw icon-dot-open',
label: 'Disable Passthrough', label: 'Disable Passthrough',
bulkable: true, bulkable: true,
@ -54,6 +54,10 @@ export default class PCIDevice extends SteveModel {
return out; return out;
} }
get canUpdate() {
return !!this.linkFor('update');
}
get isvGPUDevice() { get isvGPUDevice() {
if (!this.vGPUAsPCIDeviceFeatureEnabled) { if (!this.vGPUAsPCIDeviceFeatureEnabled) {
return false; return false;

View File

@ -16,13 +16,13 @@ export default class SRIOVDevice extends SteveModel {
out.push( out.push(
{ {
action: 'enableDevice', action: 'enableDevice',
enabled: !this.isEnabled, enabled: !this.isEnabled && this.canUpdate,
icon: 'icon icon-fw icon-dot', icon: 'icon icon-fw icon-dot',
label: 'Enable', label: 'Enable',
}, },
{ {
action: 'disableDevice', action: 'disableDevice',
enabled: this.isEnabled, enabled: this.isEnabled && this.canUpdate,
icon: 'icon icon-fw icon-dot-open', icon: 'icon icon-fw icon-dot-open',
label: 'Disable', label: 'Disable',
}, },
@ -31,6 +31,10 @@ export default class SRIOVDevice extends SteveModel {
return out; return out;
} }
get canUpdate() {
return !!this.linkFor('update');
}
get canYaml() { get canYaml() {
return false; return false;
} }

View File

@ -33,7 +33,7 @@ export default class USBDevice extends SteveModel {
out.push( out.push(
{ {
action: 'enablePassthroughBulk', action: 'enablePassthroughBulk',
enabled: !this.passthroughClaim && !this.status.enabled, enabled: !this.passthroughClaim && !this.status.enabled && this.canUpdate,
icon: 'icon icon-fw icon-dot', icon: 'icon icon-fw icon-dot',
label: 'Enable Passthrough', label: 'Enable Passthrough',
bulkable: true, bulkable: true,
@ -42,7 +42,7 @@ export default class USBDevice extends SteveModel {
}, },
{ {
action: 'disablePassthrough', action: 'disablePassthrough',
enabled: this.status.enabled, enabled: this.status.enabled && this.canUpdate,
icon: 'icon icon-fw icon-dot-open', icon: 'icon icon-fw icon-dot-open',
label: 'Disable Passthrough', label: 'Disable Passthrough',
bulkable: true, bulkable: true,
@ -53,6 +53,10 @@ export default class USBDevice extends SteveModel {
return out; return out;
} }
get canUpdate() {
return !!this.linkFor('update');
}
get canYaml() { get canYaml() {
return false; return false;
} }

View File

@ -27,17 +27,18 @@ const STATUS_DISPLAY = {
export default class VGpuDevice extends SteveModel { export default class VGpuDevice extends SteveModel {
get _availableActions() { get _availableActions() {
const out = super._availableActions; const out = super._availableActions;
const canUpdate = !!this.linkFor('update');
out.push( out.push(
{ {
action: 'enableVGpu', action: 'enableVGpu',
enabled: !this.isEnabled, enabled: !this.isEnabled && canUpdate,
icon: 'icon icon-fw icon-dot', icon: 'icon icon-fw icon-dot',
label: 'Enable', label: 'Enable',
}, },
{ {
action: 'disableVGpu', action: 'disableVGpu',
enabled: this.isEnabled, enabled: this.isEnabled && canUpdate,
icon: 'icon icon-fw icon-dot-open', icon: 'icon icon-fw icon-dot-open',
label: 'Disable', label: 'Disable',
bulkable: true, bulkable: true,

View File

@ -24,7 +24,7 @@ const OBSCURE_NAMESPACE_PREFIX = [
export default class HciNamespace extends namespace { export default class HciNamespace extends namespace {
get _availableActions() { get _availableActions() {
const out = super._availableActions; let out = super._availableActions;
const remove = out.findIndex((a) => a.action === 'promptRemove'); const remove = out.findIndex((a) => a.action === 'promptRemove');
const promptRemove = { const promptRemove = {
@ -53,6 +53,16 @@ export default class HciNamespace extends namespace {
insertAt(out, out.length - 1, promptRemove); insertAt(out, out.length - 1, promptRemove);
insertAt(out, out.length - 5, editQuotaAction); insertAt(out, out.length - 5, editQuotaAction);
const canUpdate = !!this.linkFor('update');
out = out.map((action) => {
if (['move'].includes(action.action)) {
return { ...action, enabled: action.enabled && canUpdate };
}
return action;
});
return out; return out;
} }

View File

@ -235,6 +235,10 @@ export default class HciPv extends HarvesterResource {
return allVMs.find(findAttachVM); return allVMs.find(findAttachVM);
} }
get attachVMName() {
return this.attachVM?.nameDisplay || this.attachVM?.metadata?.name || '';
}
get isAvailable() { get isAvailable() {
const unAvailable = ['Resizing', 'Not Ready']; const unAvailable = ['Resizing', 'Not Ready'];

View File

@ -5,6 +5,20 @@ import Secret from '@shell/models/secret';
import { NAMESPACE } from '@shell/config/types'; import { NAMESPACE } from '@shell/config/types';
export default class HciSecret extends Secret { export default class HciSecret extends Secret {
get _availableActions() {
let out = super._availableActions;
out = out.map((action) => {
if (['download'].includes(action.action)) {
return { ...action, enabled: !!this.linkFor('update') };
}
return action;
});
return out;
}
// prevent harvester secret detail page be overridden. // prevent harvester secret detail page be overridden.
// See isFullPageOverride in https://github.com/rancher/dashboard/blob/master/shell/components/ResourceDetail/index.vue // See isFullPageOverride in https://github.com/rancher/dashboard/blob/master/shell/components/ResourceDetail/index.vue
get fullDetailPageOverride() { get fullDetailPageOverride() {

View File

@ -96,6 +96,10 @@ export default class HciStorageClass extends StorageClass {
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption'); return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
} }
get expandOnlineEncryptedVolumeFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('expandOnlineEncryptedVolume');
}
get thirdPartyStorageFeatureEnabled() { get thirdPartyStorageFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage'); return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
} }
@ -106,6 +110,15 @@ export default class HciStorageClass extends StorageClass {
get availableActions() { get availableActions() {
let out = super.availableActions || []; let out = super.availableActions || [];
const canUpdate = !!this.linkFor('update');
out = out.map((action) => {
if (['setDefault', 'setAsDefault', 'resetDefault'].includes(action.action)) {
return { ...action, enabled: canUpdate };
}
return action;
});
if (this.isInternalStorageClass()) { if (this.isInternalStorageClass()) {
out = out.filter((action) => { out = out.filter((action) => {

View File

@ -4,6 +4,8 @@ import { HCI as HCI_ANNOTATIONS } from '../config/labels-annotations';
import HarvesterResource from './harvester'; import HarvesterResource from './harvester';
import { HCI } from '../types'; import { HCI } from '../types';
const HARVESTER_NVIDIA_DRIVER_TOOLKIT = 'harvester-system/nvidia-driver-toolkit';
export default class HciAddonConfig extends HarvesterResource { export default class HciAddonConfig extends HarvesterResource {
get availableActions() { get availableActions() {
const out = super._availableActions; const out = super._availableActions;
@ -19,9 +21,10 @@ export default class HciAddonConfig extends HarvesterResource {
out.push(rancherDashboard); out.push(rancherDashboard);
} }
const canUpdate = !!this.linkFor('update');
const toggleAddon = { const toggleAddon = {
action: 'toggleAddon', action: 'toggleAddon',
enabled: true, enabled: canUpdate,
icon: this.spec.enabled ? 'icon icon-pause' : 'icon icon-play', icon: this.spec.enabled ? 'icon icon-pause' : 'icon icon-play',
label: this.spec.enabled ? this.t('generic.disable') : this.t('generic.enable'), label: this.spec.enabled ? this.t('generic.disable') : this.t('generic.enable'),
}; };
@ -45,6 +48,15 @@ export default class HciAddonConfig extends HarvesterResource {
} }
} }
if (!this.spec.enabled && this.id === HARVESTER_NVIDIA_DRIVER_TOOLKIT) {
this.$dispatch('promptModal', {
resources: [this],
component: 'HarvesterEnableNvidiaDriverToolkit',
});
return;
}
this.spec.enabled = !this.spec.enabled; this.spec.enabled = !this.spec.enabled;
await this.save(); await this.save();
} catch (err) { } catch (err) {

View File

@ -3,6 +3,20 @@ import { findBy } from '@shell/utils/array';
import HarvesterResource from './harvester'; import HarvesterResource from './harvester';
export default class HciKeypair extends HarvesterResource { export default class HciKeypair extends HarvesterResource {
get _availableActions() {
let out = super._availableActions;
out = out.map((action) => {
if (['download'].includes(action.action)) {
return { ...action, enabled: !!this.linkFor('update') };
}
return action;
});
return out;
}
get stateDisplay() { get stateDisplay() {
const conditions = get(this, 'status.conditions'); const conditions = get(this, 'status.conditions');
const status = (findBy(conditions, 'type', 'validated') || {}).status ; const status = (findBy(conditions, 'type', 'validated') || {}).status ;

View File

@ -19,16 +19,18 @@ export default class ScheduleVmBackup extends HarvesterResource {
} }
}); });
const canUpdate = !!this.linkFor('update');
return [ return [
{ {
action: 'resumeSchedule', action: 'resumeSchedule',
enabled: ucFirst(this.state) === STATES.suspended.label, enabled: canUpdate && ucFirst(this.state) === STATES.suspended.label,
icon: 'icons icon-play', icon: 'icons icon-play',
label: this.t('harvester.action.resumeSchedule'), label: this.t('harvester.action.resumeSchedule'),
}, },
{ {
action: 'suspendSchedule', action: 'suspendSchedule',
enabled: ucFirst(this.state) === STATES.active.label, enabled: canUpdate && ucFirst(this.state) === STATES.active.label,
icon: 'icons icon-pause', icon: 'icons icon-pause',
label: this.t('harvester.action.suspendSchedule'), label: this.t('harvester.action.suspendSchedule'),
}, },

View File

@ -39,9 +39,6 @@ function isReady() {
export default class HciVmImage extends HarvesterResource { export default class HciVmImage extends HarvesterResource {
get availableActions() { get availableActions() {
let out = super._availableActions; let out = super._availableActions;
const toFilter = ['goToEditYaml'];
out = out.filter( (A) => !toFilter.includes(A.action));
// show `Clone` only when imageSource is `download` // show `Clone` only when imageSource is `download`
if (this.imageSource !== 'download') { if (this.imageSource !== 'download') {
@ -55,6 +52,7 @@ export default class HciVmImage extends HarvesterResource {
canCreateVM = false; canCreateVM = false;
} }
const canCreateImage = !!this.$getters?.['schemaFor']?.(HCI.IMAGE)?.collectionMethods?.some((method) => method.toLowerCase() === 'post');
const customActions = this.isReady ? [ const customActions = this.isReady ? [
{ {
action: 'createFromImage', action: 'createFromImage',
@ -64,13 +62,13 @@ export default class HciVmImage extends HarvesterResource {
}, },
{ {
action: 'encryptImage', action: 'encryptImage',
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted, enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted && canCreateImage,
icon: 'icon icon-lock', icon: 'icon icon-lock',
label: this.t('harvester.action.encryptImage'), label: this.t('harvester.action.encryptImage'),
}, },
{ {
action: 'decryptImage', action: 'decryptImage',
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted, enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted && canCreateImage,
icon: 'icon icon-unlock', icon: 'icon icon-unlock',
label: this.t('harvester.action.decryptImage'), label: this.t('harvester.action.decryptImage'),
}, },

View File

@ -23,17 +23,17 @@ export default class HciVmTemplateVersion extends HarvesterResource {
}); });
const schema = this.$getters['schemaFor'](HCI.VM); const schema = this.$getters['schemaFor'](HCI.VM);
let canCreateVM = true; let canCreateVM = false;
if ( schema && !schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())) ) { if (schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())) ) {
canCreateVM = false; canCreateVM = true;
} }
return [ return [
{ {
action: 'launchFromTemplate', action: 'launchFromTemplate',
icon: 'icon icon-spinner', icon: 'icon icon-spinner',
disabled: !canCreateVM || !this.isReady, enabled: canCreateVM && this.isReady,
label: this.t('harvester.action.launchFormTemplate'), label: this.t('harvester.action.launchFormTemplate'),
}, },
{ {

View File

@ -83,17 +83,60 @@ const VMIPhase = {
let productInStore; let productInStore;
let _podOwnerMap = null;
let _podOwnerMapSource = null;
function getPodByOwnerName(rootGetters, inStore, ownerName) {
const podList = rootGetters[`${ inStore }/all`](POD);
if (!Array.isArray(podList)) {
return undefined;
}
// if not equals (usually means the pod list has been updated), we need to rebuild the map, otherwise we can reuse the map for better performance
if (_podOwnerMapSource !== podList) {
_podOwnerMap = new Map(); // use Map to store ownerReference name and pod mapping
for (const pod of podList) {
const refName = pod.metadata?.ownerReferences?.[0]?.name;
if (refName) {
_podOwnerMap.set(refName, pod);
}
}
_podOwnerMapSource = podList;
}
return _podOwnerMap.get(ownerName);
}
function getPvcsByNames(rootGetters, inStore, names) {
const pvcList = rootGetters[`${ inStore }/all`](PVC);
if (!Array.isArray(pvcList)) {
return [];
}
const uniqueNames = new Set(names);
return pvcList.filter((pvc) => uniqueNames.has(pvc.metadata?.name));
}
const IgnoreMessages = ['pod has unbound immediate PersistentVolumeClaims']; const IgnoreMessages = ['pod has unbound immediate PersistentVolumeClaims'];
export default class VirtVm extends HarvesterResource { export default class VirtVm extends HarvesterResource {
get availableActions() { get availableActions() {
const out = super._availableActions; let out = super._availableActions;
if (this.isCloneBackendStorageCloning || this.isCloneBackendStorageFailed) {
out = out.filter(({ action }) => action !== 'goToClone');
}
const clone = out.find((action) => action.action === 'goToClone'); const clone = out.find((action) => action.action === 'goToClone');
if (clone) { if (clone) {
clone.action = 'goToCloneVM'; clone.action = 'goToCloneVM';
} }
const canCreateVMSSchedule = !!this.$getters?.['schemaFor']?.(HCI.SCHEDULE_VM_BACKUP)?.collectionMethods?.find((x) => ['post'].includes(x.toLowerCase()));
return [ return [
{ {
action: 'stopVM', action: 'stopVM',
@ -126,6 +169,7 @@ export default class VirtVm extends HarvesterResource {
}, },
{ {
action: 'restartVM', action: 'restartVM',
altAction: 'altRestartVM',
enabled: !!this.actions?.restart, enabled: !!this.actions?.restart,
icon: 'icon icon-refresh', icon: 'icon icon-refresh',
label: this.t('harvester.action.restart'), label: this.t('harvester.action.restart'),
@ -134,6 +178,7 @@ export default class VirtVm extends HarvesterResource {
}, },
{ {
action: 'softrebootVM', action: 'softrebootVM',
altAction: 'doSoftReboot',
enabled: !!this.actions?.softreboot, enabled: !!this.actions?.softreboot,
icon: 'icon icon-pipeline', icon: 'icon icon-pipeline',
label: this.t('harvester.action.softreboot') label: this.t('harvester.action.softreboot')
@ -143,7 +188,8 @@ export default class VirtVm extends HarvesterResource {
enabled: !!this.actions?.start, enabled: !!this.actions?.start,
icon: 'icon icon-play', icon: 'icon icon-play',
label: this.t('harvester.action.start'), label: this.t('harvester.action.start'),
bulkable: true bulkable: true,
bulkAction: 'startVM'
}, },
{ {
action: 'backupVM', action: 'backupVM',
@ -171,7 +217,7 @@ export default class VirtVm extends HarvesterResource {
}, },
{ {
action: 'createSchedule', action: 'createSchedule',
enabled: this.schedulingVMBackupFeatureEnabled, enabled: canCreateVMSSchedule && this.schedulingVMBackupFeatureEnabled,
icon: 'icon icon-history', icon: 'icon icon-history',
label: this.t('harvester.action.createSchedule') label: this.t('harvester.action.createSchedule')
}, },
@ -191,7 +237,9 @@ export default class VirtVm extends HarvesterResource {
action: 'migrateVM', action: 'migrateVM',
enabled: !!this.actions?.migrate, enabled: !!this.actions?.migrate,
icon: 'icon icon-copy', icon: 'icon icon-copy',
label: this.t('harvester.action.migrate') label: this.t('harvester.action.vmMigrate'),
bulkable: true,
bulkAction: 'migrateVM'
}, },
{ {
action: 'abortMigrationVM', action: 'abortMigrationVM',
@ -502,16 +550,38 @@ export default class VirtVm extends HarvesterResource {
}); });
} }
altStopVM() { async altRestartVM() {
this.doActionGrowl('stop', {}); await this.doActionGrowl('restart', {});
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
} }
forceStop() { async altStopVM() {
this.doActionGrowl('forceStop', {}); await this.doActionGrowl('stop', {});
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
} }
startVM() { async forceStop() {
this.doActionGrowl('start', {}); await this.doActionGrowl('forceStop', {});
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
}
async startVM(resources = this) {
const list = Array.isArray(resources) ? resources : [resources];
for (const r of list) {
await r.doActionGrowl('start', {});
}
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
}
async download() {
await super.download();
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
}
async downloadBulk(items) {
await super.downloadBulk(items);
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
} }
migrateVM(resources = this) { migrateVM(resources = this) {
@ -660,16 +730,13 @@ export default class VirtVm extends HarvesterResource {
get podResource() { get podResource() {
const inStore = this.productInStore; const inStore = this.productInStore;
const vmiResource = this.$rootGetters[`${ inStore }/byId`](HCI.VMI, this.id); const vmiResource = this.$rootGetters[`${ inStore }/byId`](HCI.VMI, this.id);
const podList = this.$rootGetters[`${ inStore }/all`](POD);
return podList.find((P) => { if (!vmiResource?.metadata?.name) {
return ( return undefined;
vmiResource?.metadata?.name && }
vmiResource?.metadata?.name === P.metadata?.ownerReferences?.[0].name
); return getPodByOwnerName(this.$rootGetters, inStore, vmiResource.metadata.name);
});
} }
get isPaused() { get isPaused() {
@ -710,17 +777,13 @@ export default class VirtVm extends HarvesterResource {
get vmi() { get vmi() {
const inStore = this.productInStore; const inStore = this.productInStore;
const vmis = this.$rootGetters[`${ inStore }/all`](HCI.VMI); return this.$rootGetters[`${ inStore }/byId`](HCI.VMI, this.id);
return vmis.find((VMI) => VMI.id === this.id);
} }
get volumes() { get volumes() {
const pvcs = this.$rootGetters[`${ this.productInStore }/all`](PVC);
const volumeClaimNames = this.spec.template.spec.volumes?.map((v) => v.persistentVolumeClaim?.claimName).filter((v) => !!v) || []; const volumeClaimNames = this.spec.template.spec.volumes?.map((v) => v.persistentVolumeClaim?.claimName).filter((v) => !!v) || [];
return pvcs.filter((pvc) => volumeClaimNames.includes(pvc.metadata.name)); return getPvcsByNames(this.$rootGetters, this.productInStore, volumeClaimNames);
} }
get lvmVolumes() { get lvmVolumes() {
@ -731,6 +794,18 @@ export default class VirtVm extends HarvesterResource {
return this.volumes.filter((volume) => volume?.isLonghornV2); return this.volumes.filter((volume) => volume?.isLonghornV2);
} }
get cloneBackendStorageStatus() {
return this.metadata?.annotations?.[HCI_ANNOTATIONS.CLONE_BACKEND_STORAGE_STATUS]?.toLowerCase() || '';
}
get isCloneBackendStorageCloning() {
return this.cloneBackendStorageStatus === 'cloning';
}
get isCloneBackendStorageFailed() {
return this.cloneBackendStorageStatus === 'failed';
}
get encryptedVolumeType() { get encryptedVolumeType() {
if (!this.volumes || this.volumes.length === 0) { if (!this.volumes || this.volumes.length === 0) {
return 'none'; return 'none';
@ -753,17 +828,6 @@ export default class VirtVm extends HarvesterResource {
return { status: 'VMI error', detailedMessage: vmiFailureCond.message }; return { status: 'VMI error', detailedMessage: vmiFailureCond.message };
} }
if ((this.vmi || this.isVMCreated) && this.podResource) {
// const podStatus = this.podResource.getPodStatus;
// if (POD_STATUS_ALL_ERROR.includes(podStatus?.status)) {
// return {
// ...podStatus,
// status: 'LAUNCHER_POD_ERROR',
// pod: this.podResource,
// };
// }
}
return this?.vmi?.status?.phase; return this?.vmi?.status?.phase;
} }
@ -797,13 +861,21 @@ export default class VirtVm extends HarvesterResource {
!this.isVMExpectedRunning && !this.isVMExpectedRunning &&
this.isVMCreated && this.isVMCreated &&
this.vmi?.status?.phase === VMIPhase.Pending this.vmi?.status?.phase === VMIPhase.Pending
) || (this.metadata?.annotations?.[HCI_ANNOTATIONS.CLONE_BACKEND_STORAGE_STATUS] === 'cloning')) { ) || this.isCloneBackendStorageCloning) {
return { status: VMIPhase.Pending }; return { status: VMIPhase.Pending };
} }
return null; return null;
} }
get isCloneFailed() {
if (this.isCloneBackendStorageFailed) {
return { status: VMIPhase.Failed };
}
return null;
}
get isStopping() { get isStopping() {
if (this && if (this &&
!this.isVMExpectedRunning && !this.isVMExpectedRunning &&
@ -901,9 +973,7 @@ export default class VirtVm extends HarvesterResource {
const inStore = this.productInStore; const inStore = this.productInStore;
const allRestore = this.$rootGetters[`${ inStore }/all`](HCI.RESTORE); const res = this.$rootGetters[`${ inStore }/byId`](HCI.RESTORE, id);
const res = allRestore.find((O) => O.id === id);
if (res) { if (res) {
const allBackups = this.$rootGetters[`${ inStore }/all`](HCI.BACKUP); const allBackups = this.$rootGetters[`${ inStore }/all`](HCI.BACKUP);
@ -962,6 +1032,7 @@ export default class VirtVm extends HarvesterResource {
this.isUnschedulable?.status || this.isUnschedulable?.status ||
this.isPaused?.status || this.isPaused?.status ||
this.isVMError?.status || this.isVMError?.status ||
this.isCloneFailed?.status ||
this.isPending?.status || this.isPending?.status ||
this.isStopping?.status || this.isStopping?.status ||
this.isOff?.status || this.isOff?.status ||
@ -969,7 +1040,7 @@ export default class VirtVm extends HarvesterResource {
this.isRunning?.status || this.isRunning?.status ||
this.isNotReady?.status || this.isNotReady?.status ||
this.isStarting?.status || this.isStarting?.status ||
this.isWaitingForVMI?.state || this.isWaitingForVMI?.status ||
this.otherState?.status; this.otherState?.status;
return state; return state;
@ -1073,42 +1144,6 @@ export default class VirtVm extends HarvesterResource {
return out; return out;
} }
get warningCount() {
return this.resourcesStatus.warningCount;
}
get errorCount() {
return this.resourcesStatus.errorCount;
}
get resourcesStatus() {
const inStore = this.productInStore;
const vmList = this.$rootGetters[`${ inStore }/all`](HCI.VM);
let warningCount = 0;
let errorCount = 0;
vmList.forEach((vm) => {
const status = vm.actualState;
if (status === VM_ERROR) {
errorCount += 1;
} else if (
status === 'Stopping' ||
status === 'Waiting' ||
status === 'Pending' ||
status === 'Starting' ||
status === 'Terminating'
) {
warningCount += 1;
}
});
return {
warningCount,
errorCount
};
}
get volumeClaimTemplates() { get volumeClaimTemplates() {
return parseVolumeClaimTemplates(this); return parseVolumeClaimTemplates(this);
} }
@ -1126,7 +1161,6 @@ export default class VirtVm extends HarvesterResource {
get rootImageId() { get rootImageId() {
let imageId = ''; let imageId = '';
const inStore = this.productInStore; const inStore = this.productInStore;
const pvcs = this.$rootGetters[`${ inStore }/all`](PVC) || [];
const volumes = this.spec.template.spec.volumes || []; const volumes = this.spec.template.spec.volumes || [];
@ -1136,9 +1170,7 @@ export default class VirtVm extends HarvesterResource {
}); });
if (!isNoExistingVolume) { if (!isNoExistingVolume) {
const existingVolume = pvcs.find( const existingVolume = this.$rootGetters[`${ inStore }/byId`](PVC, `${ this.metadata.namespace }/${ firstVolumeName }`);
(P) => P.id === `${ this.metadata.namespace }/${ firstVolumeName }`
);
if (existingVolume) { if (existingVolume) {
return existingVolume?.metadata?.annotations?.[ return existingVolume?.metadata?.annotations?.[
@ -1287,6 +1319,10 @@ export default class VirtVm extends HarvesterResource {
return this.$rootGetters['harvester-common/getFeatureEnabled']('schedulingVMBackup'); return this.$rootGetters['harvester-common/getFeatureEnabled']('schedulingVMBackup');
} }
get nameDisplay() {
return this.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME] || this.metadata?.name || this.id;
}
get volumeEncryptionFeatureEnabled() { get volumeEncryptionFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption'); return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
} }
@ -1316,8 +1352,7 @@ export default class VirtVm extends HarvesterResource {
} }
get isBackupTargetUnavailable() { get isBackupTargetUnavailable() {
const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || []; const backupTargetSetting = this.$rootGetters['harvester/byId'](HCI.SETTING, 'backup-target');
const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target');
return isBackupTargetSettingUnavailable(backupTargetSetting); return isBackupTargetSettingUnavailable(backupTargetSetting);
} }

View File

@ -0,0 +1,19 @@
import shellProject from '@shell/models/management.cattle.io.project';
// This model controls `Project / Namespace` page in rancher integration mode
// Extend management.cattle.io.project model from shell
export default class Project extends shellProject {
get _availableActions() {
const canUpdate = !!this.linkFor('update');
// disable `Edit Config` action if user does not have update permission.
return super._availableActions.map((action) => {
if (action.action === 'goToEdit') {
return { ...action, enabled: canUpdate };
}
return action;
});
}
}

View File

@ -57,7 +57,11 @@ export default class HciVlanConfig extends HarvesterResource {
get _availableActions() { get _availableActions() {
const out = super._availableActions; const out = super._availableActions;
const canMigrate = !!this.$getters?.['schemaFor']?.(HCI.VLAN_CONFIG)?.collectionMethods?.find((x) => ['post'].includes(x.toLowerCase()));
if (canMigrate) {
insertAt(out, 0, this.migrateAction); insertAt(out, 0, this.migrateAction);
}
return out; return out;
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "harvester", "name": "harvester",
"description": "Rancher UI Extension for Harvester", "description": "Rancher UI Extension for Harvester",
"version": "1.8.1-dev", "version": "1.9.0-dev",
"private": false, "private": false,
"rancher": { "rancher": {
"annotations": { "annotations": {

View File

@ -123,6 +123,7 @@ export default {
this.value?.[0]?.currentRouter().push(goTo); this.value?.[0]?.currentRouter().push(goTo);
} }
this.close(); this.close();
this.$store.commit('action-menu/togglePromptModal', { performCallback: true, clearTableSelection: true });
}).catch((err) => { }).catch((err) => {
this.$emit('errors', err); this.$emit('errors', err);
}); });

View File

@ -3,7 +3,8 @@ import { ClusterNotFoundError } from '@shell/utils/error';
import { SETTING } from '@shell/config/settings'; import { SETTING } from '@shell/config/settings';
import { COUNT, NAMESPACE, MANAGEMENT } from '@shell/config/types'; import { COUNT, NAMESPACE, MANAGEMENT } from '@shell/config/types';
import { allHash } from '@shell/utils/promise'; import { allHash } from '@shell/utils/promise';
import { DEV } from '@shell/store/prefs'; import { DEV, NAMESPACE_FILTERS } from '@shell/store/prefs';
import { createNamespaceFilterKeyWithId } from '@shell/utils/namespace-filter';
import { HCI } from '../../types'; import { HCI } from '../../types';
export default { export default {
@ -121,8 +122,11 @@ export default {
await dispatch('cleanNamespaces', null, { root: true }); await dispatch('cleanNamespaces', null, { root: true });
const namespaceFilterKey = createNamespaceFilterKeyWithId(id, 'harvester');
const savedFilters = rootGetters['prefs/get'](NAMESPACE_FILTERS)?.[namespaceFilterKey];
commit('updateNamespaces', { commit('updateNamespaces', {
filters: [], filters: savedFilters || [],
all: getters.filterNamespace(), all: getters.filterNamespace(),
getters getters
}, { root: true }); }, { root: true });

View File

@ -123,5 +123,26 @@ export default {
const clusterId = currentCluster.id; const clusterId = currentCluster.id;
return projectsInAllClusters.filter((project: any) => project.spec.clusterName === clusterId && project.nameDisplay !== 'System'); return projectsInAllClusters.filter((project: any) => project.spec.clusterName === clusterId && project.nameDisplay !== 'System');
},
// Few harvester resources name and REAL resource are different. E.g. HCI.NETWORK_ATTACHMENT page resource is NETWORK_ATTACHMENT.
// Check in config/harvester-cluster.js for more details.
// We need to look up the schema by resource name, and fallback to find using real resource name
schemaFor: (state, getters, rootState, rootGetters) => (type, _fuzzy = false, _allowThrow = true) => {
// follow the same logic as type-map/schemaFor in /dashboard/shell/plugins/dashboard-store/getters.js
const normalizedType = getters.normalizeType(type);
const schemas = state.types['schema'];
const out = schemas?.map?.get(normalizedType);
if (out) return out;
// if not found, use the resource mapping in configureType for a second try
const resourceType = rootGetters['type-map/optionsFor'](type)?.resource;
if (resourceType && resourceType !== type) {
const normalizedResource = getters.normalizeType(resourceType);
return schemas?.map?.get(normalizedResource) || null;
} }
return null;
},
}; };

View File

@ -1,3 +1,9 @@
// To find the CRD name, you can run `kubectl api-resources` and look for the `NAME` column.
// The CRD name is usually in the format of `<plural>.<group>`, where `<plural>` is the plural form of the resource and `<group>` is the API group it belongs to.
// e.g
// 1. `virtualmachines.kubevirt.io` -> kubevirt.io.virtualmachine, the CRD name for the `VirtualMachine` resource in the `kubevirt.io` API group
// 2. `vpc-nat-gateways.kubeovn.io` -> kubeovn.io.vpcnatgateway, the CRD name for the `VpcNatGateway` resource in the `kubeovn.io` API group.
export const HCI = { export const HCI = {
VM: 'kubevirt.io.virtualmachine', VM: 'kubevirt.io.virtualmachine',
VMI: 'kubevirt.io.virtualmachineinstance', VMI: 'kubevirt.io.virtualmachineinstance',
@ -16,9 +22,16 @@ export const HCI = {
RESTORE: 'harvesterhci.io.virtualmachinerestore', RESTORE: 'harvesterhci.io.virtualmachinerestore',
NODE_NETWORK: 'network.harvesterhci.io.nodenetwork', NODE_NETWORK: 'network.harvesterhci.io.nodenetwork',
CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork', CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork',
HOST_NETWORK_CONFIG: 'network.harvesterhci.io.hostnetworkconfig',
SUBNET: 'kubeovn.io.subnet', SUBNET: 'kubeovn.io.subnet',
VPC: 'kubeovn.io.vpc', VPC: 'kubeovn.io.vpc',
IP: 'kubeovn.io.ip', IP: 'kubeovn.io.ip',
VLAN: 'kubeovn.io.vlan',
IPTABLES_EIP: 'kubeovn.io.iptableseip',
IPTABLES_SNAT_RULE: 'kubeovn.io.iptablessnatrule',
IPTABLES_DNAT_RULE: 'kubeovn.io.iptablesdnatrule',
PROVIDER_NETWORK: 'kubeovn.io.providernetwork',
VPC_NAT_GATEWAY: 'kubeovn.io.vpcnatgateway',
VM_IMAGE_DOWNLOADER: 'harvesterhci.io.virtualmachineimagedownloader', VM_IMAGE_DOWNLOADER: 'harvesterhci.io.virtualmachineimagedownloader',
SUPPORT_BUNDLE: 'harvesterhci.io.supportbundle', SUPPORT_BUNDLE: 'harvesterhci.io.supportbundle',
NETWORK_ATTACHMENT: 'harvesterhci.io.networkattachmentdefinition', NETWORK_ATTACHMENT: 'harvesterhci.io.networkattachmentdefinition',
@ -62,6 +75,7 @@ export const HCI = {
VMIMPORT_SOURCE_OVA: 'migration.harvesterhci.io.ovasource', VMIMPORT_SOURCE_OVA: 'migration.harvesterhci.io.ovasource',
VMIMPORT: 'migration.harvesterhci.io.virtualmachineimport', VMIMPORT: 'migration.harvesterhci.io.virtualmachineimport',
MIGRATION: 'migration.harvesterhci.io', MIGRATION: 'migration.harvesterhci.io',
}; };
export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot'; export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot';

View File

@ -31,22 +31,42 @@ export function registerAddonSideNav(store, productName, {
}, 600); }, 600);
}; };
// Adds or removes the resource IDs from the product visibility whitelist. const hasAccessibleSchema = (t) => {
const setMenuVisibility = (visible) => { try {
if (visible) { return !!store.getters[`${ productName }/schemaFor`]?.(t);
} catch (e) {
return false;
}
};
const showTypes = (visibleTypes) => {
store.commit('type-map/basicType', { store.commit('type-map/basicType', {
product: productName, product: productName,
group: navGroup, group: navGroup,
types types: visibleTypes
}); });
} else { };
// Manually delete the keys from the state object to hide them.
const hideTypes = () => {
const basicTypes = store.state['type-map'].basicTypes[productName]; const basicTypes = store.state['type-map'].basicTypes[productName];
if (basicTypes) { if (basicTypes) {
types.forEach((t) => delete basicTypes[t]); types.forEach((t) => delete basicTypes[t]);
} }
};
// Adds or removes the resource IDs from the product visibility whitelist.
const setMenuVisibility = (visible) => {
const accessibleTypes = visible ? types.filter(hasAccessibleSchema) : [];
// Always clear first to remove any previously-registered types that are
// no longer accessible (e.g. partial permission changes like types=[A,B] where B is dropped).
hideTypes();
if (accessibleTypes.length > 0) {
showTypes(accessibleTypes);
} }
kickSideNav(); kickSideNav();
}; };

View File

@ -0,0 +1,21 @@
const AUTH_ERROR_CODES = [401, 403, 404];
export function getLoginAwareErrors(err, message = '') {
const errors = Array.isArray(err) ? err : (err ? [err] : []);
if (!errors.length) {
return [];
}
const generic = message;
if (errors.some((e) => AUTH_ERROR_CODES.includes(e?._status || e?.response?.status))) {
return [generic];
}
const msgs = errors
.map((e) => (typeof e === 'string' ? e : (e?.message || e?._statusText || '')))
.filter(Boolean);
return msgs.length ? msgs : [generic];
}

View File

@ -27,23 +27,6 @@ OUTPUT_DIR=dist/${DIR}-embedded
echo "Building..." echo "Building..."
COMMIT=${COMMIT} VERSION=${VERSION} OUTPUT_DIR=$OUTPUT_DIR ROUTER_BASE='/dashboard/' RESOURCE_BASE='/dashboard/' RANCHER_ENV=harvester yarn run build COMMIT=${COMMIT} VERSION=${VERSION} OUTPUT_DIR=$OUTPUT_DIR ROUTER_BASE='/dashboard/' RESOURCE_BASE='/dashboard/' RANCHER_ENV=harvester yarn run build
if [ -v EMBED_PKG ]; then
echo "Build and embed plugin from: $EMBED_PKG"
PKG_FILE_NAME=${EMBED_PKG##*/}
echo PKG_FILE_NAME: $PKG_FILE_NAME
PKG_NAME="${PKG_FILE_NAME/.tar.gz/""}"
echo "Plugin name: '$PKG_NAME'"
# Fetch file, unpack and move to dist
curl $EMBED_PKG --output $PKG_FILE_NAME
OUTPUT_DIR_PKG=$OUTPUT_DIR/$PKG_NAME
mkdir -p $OUTPUT_DIR_PKG
tar xvfz $PKG_FILE_NAME -C $OUTPUT_DIR/$PKG_NAME
echo "Plugin contents that will be served from $PKG_NAME"
ls -alR $OUTPUT_DIR/$PKG_NAME
fi
echo "Destroying..." echo "Destroying..."
find $OUTPUT_DIR -name "index.html" -mindepth 2 -exec rm {} \; find $OUTPUT_DIR -name "index.html" -mindepth 2 -exec rm {} \;
find $OUTPUT_DIR -type d -empty -depth -exec rmdir {} \; find $OUTPUT_DIR -type d -empty -depth -exec rmdir {} \;

3029
yarn.lock

File diff suppressed because it is too large Load Diff