Compare commits

..
Author SHA1 Message Date
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 b34d618c7c.

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 LeeandCopilot 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 15eb0f07f7.
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
freezeandAndy Lee 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
58 changed files with 2548 additions and 2592 deletions
+13 -4
View File
@@ -39,11 +39,20 @@
"reviewers": ["a110605", "houhoucoop"]
},
{
"matchUpdateTypes": ["patch", "digest"],
"automerge": true,
"matchUpdateTypes": ["patch"],
"automerge": false,
"minimumReleaseAge": "7 days",
"groupName": "patch digest dependencies",
"labels": ["patch-update", "automerge"]
"groupName": "patch dependencies",
"labels": ["patch-update"],
"reviewers": ["a110605", "houhoucoop"]
},
{
"matchUpdateTypes": ["digest", "pinDigest"],
"automerge": false,
"groupName": "digest dependencies",
"labels": ["digest-update"],
"schedule": ["on the first day of the month"],
"reviewers": ["a110605", "houhoucoop"]
}
]
}
@@ -43,10 +43,12 @@ jobs:
needs:
- setup-release-tag
- check-version
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@9bf23dff6650116673346447bcf0b088e0edb27f # creators-pkg-v3.0.12
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@3b26a36bad555e5e2b8634b24823be29732f287c # master
permissions:
actions: write
contents: write
pull-requests: write
deployments: write
pages: write
with:
target_branch: gh-pages
tagged_release: '${{ needs.setup-release-tag.outputs.release_tag }}'
+1 -1
View File
@@ -33,7 +33,7 @@ module.exports = {
'subject-full-stop': [2, 'never', '.'],
'subject-max-length': [0, 'never'],
'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-max-line-length': [2, 'always', 100],
},
+7 -7
View File
@@ -1,13 +1,13 @@
{
"name": "harvester-ui-extension",
"version": "1.8.2",
"version": "1.9.0-dev",
"private": false,
"engines": {
"node": ">=24.0.0"
},
"dependencies": {
"@babel/plugin-transform-class-static-block": "7.29.7",
"@rancher/shell": "3.0.12-rc.1",
"@babel/plugin-transform-class-static-block": "7.28.6",
"@rancher/shell": "3.0.12-rc.3",
"@vue-flow/background": "^1.3.0",
"@vue-flow/controls": "^1.1.1",
"@vue-flow/core": "^1.33.5",
@@ -21,7 +21,7 @@
"yaml": "^2.5.1"
},
"resolutions": {
"@types/node": "25.9.5",
"@types/node": "25.9.1",
"cronstrue": "2.59.0",
"d3-color": "3.1.0",
"ejs": "3.1.10",
@@ -33,9 +33,9 @@
"merge": "2.1.1",
"node-forge": "1.4.0",
"nth-check": "2.1.1",
"qs": "6.15.3",
"roarr": "7.21.7",
"semver": "7.8.5",
"qs": "6.15.2",
"roarr": "7.21.5",
"semver": "7.8.1",
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
},
"scripts": {
+1 -1
View File
@@ -122,7 +122,7 @@ export default {
return this.$store.getters['currentCluster'].isLocal;
},
canEditClusterMembers() {
return this.normanClusterRTBSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
return this.schema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
},
},
};
+8 -5
View File
@@ -58,11 +58,14 @@ export default {
const url = `https://${ host }${ prefix }/${ PRODUCT_NAME }/c/${ params.cluster }/console/${ uid }/${ type }`;
window.open(
url,
'_blank',
`toolbars=0,width=${ screen.width - 200 },height=${ screen.height - 200 },left=0,top=0,noreferrer`
);
// Defer so v-select can finish closing the dropdown before the popup steals focus
this.$nextTick(() => {
window.open(
url,
'_blank',
`toolbars=0,width=${ screen.width - 200 },height=${ screen.height - 200 },left=0,top=0,noreferrer`
);
});
},
isEmpty(o) {
@@ -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>
+8 -1
View File
@@ -65,7 +65,14 @@ const FEATURE_FLAGS = {
'vGPUAsPCIDevice',
'instanceManagerResourcesSetting',
'rwxNetworkSetting',
'createPVCWithDataVolume'
'createPVCWithDataVolume',
'clusterPodSecurityStandardSetting',
],
'v1.8.1': [],
'v1.9.0': [
'supportFilesystem',
'disableResourcePooling',
'expandOnlineEncryptedVolume'
],
};
+9 -1
View File
@@ -237,6 +237,7 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimport',
group: 'vmimport',
namespaced: true,
ifHaveType: HCI.VMIMPORT,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT }
@@ -266,6 +267,7 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare',
group: 'vmimport',
namespaced: true,
ifHaveType: HCI.VMIMPORT_SOURCE_V,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_V }
@@ -295,6 +297,7 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack',
group: 'vmimport',
namespaced: true,
ifHaveType: HCI.VMIMPORT_SOURCE_O,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_O }
@@ -323,6 +326,7 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA',
group: 'vmimport',
namespaced: true,
ifHaveType: HCI.VMIMPORT_SOURCE_OVA,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_OVA }
@@ -484,6 +488,7 @@ export function init($plugin, store) {
});
virtualType({
ifHaveType: LOGGING.CLUSTER_FLOW,
labelKey: 'harvester.logging.clusterFlow.label',
name: HCI.CLUSTER_FLOW,
namespaced: true,
@@ -507,6 +512,7 @@ export function init($plugin, store) {
});
virtualType({
ifHaveType: LOGGING.CLUSTER_OUTPUT,
labelKey: 'harvester.logging.clusterOutput.label',
name: HCI.CLUSTER_OUTPUT,
namespaced: true,
@@ -530,6 +536,7 @@ export function init($plugin, store) {
});
virtualType({
ifHaveType: LOGGING.FLOW,
labelKey: 'harvester.logging.flow.label',
name: HCI.FLOW,
namespaced: true,
@@ -553,6 +560,7 @@ export function init($plugin, store) {
});
virtualType({
ifHaveType: LOGGING.OUTPUT,
labelKey: 'harvester.logging.output.label',
name: HCI.OUTPUT,
namespaced: true,
@@ -686,7 +694,7 @@ export function init($plugin, store) {
},
resource: NETWORK_ATTACHMENT,
resourceDetail: HCI.NETWORK_ATTACHMENT,
resourceEdit: HCI.NETWORK_ATTACHMENT
resourceEdit: HCI.NETWORK_ATTACHMENT,
});
virtualType({
+2
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_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_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
@@ -19,6 +19,7 @@ export const HCI = {
NETWORK_TYPE: 'network.harvesterhci.io/type',
VM_NAME: 'harvesterhci.io/vmName',
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
VM_DISPLAY_NAME: 'harvesterhci.io/vmDisplayName',
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
+5 -1
View File
@@ -41,7 +41,8 @@ export const HCI_SETTING = {
RANCHER_CLUSTER: 'rancher-cluster',
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
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 = {
@@ -130,6 +131,9 @@ export const HCI_ALLOWED_SETTINGS = {
},
[HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: {
kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting'
},
[HCI_SETTING.CLUSTER_POD_SECURITY_STANDARD]: {
kind: 'json', from: 'import', canReset: true, featureFlag: 'clusterPodSecurityStandardSetting'
}
};
+6
View File
@@ -46,3 +46,9 @@ export const CDI_POPULATOR_KIND = {
VOLUME_IMPORT_SOURCE: 'VolumeImportSource',
VOLUME_CLONE_SOURCE: 'VolumeCloneSource',
};
export const FILESYSTEM_SOURCE_TYPE = {
CONFIGMAP: 'configmap',
SECRET: 'secret',
SERVICEACCOUNT: 'serviceaccount',
};
@@ -27,15 +27,11 @@ export default {
await allHash({
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
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: {
allClusterNetwork() {
return this.$store.getters['harvester/all'](HCI.CLUSTER_NETWORK);
},
rows() {
const vms = this.$store.getters['harvester/all'](HCI.VM);
@@ -108,7 +104,6 @@ export default {
<HarvesterVmState
class="vmstate"
:row="scope.row"
:all-cluster-network="allClusterNetwork"
/>
</div>
</template>
+29 -6
View File
@@ -1,6 +1,8 @@
<script>
import { mapGetters } from 'vuex';
import { Banner } from '@components/Banner';
import { Card } from '@components/Card';
import { Checkbox } from '@components/Form/Checkbox';
import AsyncButton from '@shell/components/AsyncButton';
import { escapeHtml } from '@shell/utils/string';
import { HCI } from '../types';
@@ -13,7 +15,9 @@ export default {
components: {
AsyncButton,
Banner,
Card,
Checkbox,
},
props: {
@@ -24,10 +28,16 @@ export default {
},
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: {
close() {
@@ -52,9 +62,10 @@ export default {
}]
},
spec: {
address: actionResource.status.address,
nodeName: actionResource.status.nodeName,
userName
address: actionResource.status.address,
nodeName: actionResource.status.nodeName,
userName,
disableResourcePooling: this.disableResourcePooling,
}
} );
@@ -85,7 +96,19 @@ export default {
</template>
<template #body>
{{ t('harvester.pci.enablePassthroughWarning') }}
<p class="mb-20">
{{ 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 #actions>
@@ -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>
@@ -1,15 +1,12 @@
<script>
import { mapGetters } from 'vuex';
import { NODE } from '@shell/config/types';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI } from '../types';
export default {
emits: ['close'],
@@ -62,28 +59,46 @@ export default {
return this.resources[0];
},
vmi() {
const inStore = this.$store.getters['currentProduct'].inStore;
const vmiResources = this.$store.getters[`${ inStore }/all`](HCI.VMI);
const resource = vmiResources.find((VMI) => VMI.id === this.actionResource?.id) || null;
anyCpuPinning() {
return this.resources.some((r) => r.isCpuPinning);
},
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() {
return this.t('harvester.virtualMachine.cpuPinning.migrationMessage');
},
allVmsOnTargetNode() {
if (!this.nodeName) {
return false;
}
return this.resources.every((r) => r.nodeName === this.nodeName);
},
nodeNameList() {
const nodes = this.$store.getters['harvester/all'](NODE);
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 isCpuPinning = this.actionResource?.isCpuPinning;
const matchingCpuManagerConfig = !isCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
const matchingCpuManagerConfig = !this.anyCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
return isNotSelfNode && isNotWitnessNode && matchingCpuManagerConfig;
return isNotWitnessNode && matchingCpuManagerConfig;
}).map((n) => {
let label = n?.metadata?.name;
const value = n?.metadata?.name;
@@ -126,7 +141,29 @@ export default {
}
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);
this.close();
@@ -146,17 +183,35 @@ export default {
<template>
<Card :show-highlight-border="false">
<template #title>
{{ t('harvester.modal.migration.title') }}
{{ t('harvester.modal.migration.vmMigrationTitle', { count: resources.length }) }}
</template>
<template #body>
<Banner
v-if="actionResource?.isCpuPinning"
v-if="anyCpuPinning"
color="warning"
: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
v-model:value="nodeName"
class="mt-15"
:label="t('harvester.modal.migration.fields.nodeName.label')"
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
:options="nodeNameList"
@@ -183,7 +238,7 @@ export default {
<AsyncButton
mode="apply"
:disabled="!nodeName"
:disabled="!nodeName || allVmsOnTargetNode"
@click="apply"
/>
</div>
@@ -201,4 +256,16 @@ export default {
justify-content: flex-end;
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>
+10 -2
View File
@@ -8,6 +8,7 @@ import FileSelector, { createOnSelected } from '@shell/components/form/FileSelec
import { randomStr } from '@shell/utils/string';
import CreateEditView from '@shell/mixins/create-edit-view';
import { getLoginAwareErrors } from '../utils/error';
export default {
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') },
};
</script>
@@ -72,10 +81,9 @@ export default {
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:errors="normalizedErrors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<div class="header mb-20">
<FileSelector
@@ -3,8 +3,9 @@ import KeyValue from '@shell/components/form/KeyValue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
import RadioGroup from '@components/Form/Radio/RadioGroup';
import Checkbox from '@components/Form/Checkbox/Checkbox';
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 { clone } from '@shell/utils/object';
import { uniq } from '@shell/utils/array';
@@ -16,7 +17,9 @@ const {
CSI_NODE_PUBLISH_SECRET_NAME,
CSI_NODE_PUBLISH_SECRET_NAMESPACE,
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;
export default {
@@ -27,6 +30,7 @@ export default {
LabeledSelect,
LabeledInput,
RadioGroup,
Checkbox,
},
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: {
@@ -98,8 +105,11 @@ export default {
}, []);
},
isEdit() {
return this.realMode === _EDIT;
},
isView() {
return this.mode === _VIEW;
return this.realMode === _VIEW;
},
migratableOptions() {
@@ -152,6 +162,10 @@ export default {
}
},
enableOnlineExpansionVolumeEncryption() {
return this.value.expandOnlineEncryptedVolumeFeatureEnabled;
},
volumeEncryption: {
set(neu) {
this.value['parameters'] = {
@@ -180,6 +194,11 @@ export default {
set(selectedSecret) {
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,
[CSI_PROVISIONER_SECRET_NAME]: name,
@@ -187,7 +206,8 @@ export default {
[CSI_NODE_STAGE_SECRET_NAME]: name,
[CSI_PROVISIONER_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>
<template>
@@ -337,6 +383,16 @@ export default {
:mode="mode"
/>
</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>
</template>
<KeyValue
@@ -254,15 +254,24 @@ export default {
v-if="restoreNewVm"
v-model:value="restoreResource.spec.keepMacAddress"
type="checkbox"
class="check mb-20"
:label="t('harvester.backup.restore.keepMacAddress')"
/>
<LabeledSelect
v-if="!restoreNewVm"
v-model:value="deletionPolicy"
class="mb-20"
:label="t('harvester.backup.restore.deletePreviousVolumes')"
:options="deletionPolicyOption"
/>
<Checkbox
v-model:value="restoreResource.spec.haltAfterRestore"
type="checkbox"
class="check mb-20"
:label="t('harvester.backup.restore.haltAfterRestore')"
/>
</div>
<Footer
@@ -26,6 +26,7 @@ import CpuMemory from './kubevirt.io.virtualmachine/VirtualMachineCpuMemory';
import CpuModel from './kubevirt.io.virtualmachine/VirtualMachineCpuModel';
import CloudConfig from './kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
import SSHKey from './kubevirt.io.virtualmachine/VirtualMachineSSHKey';
import Filesystem from './kubevirt.io.virtualmachine/VirtualMachineFilesystem';
export default {
name: 'HarvesterEditVMTemplate',
@@ -51,6 +52,7 @@ export default {
UnitInput,
Banner,
KeyValue,
Filesystem,
},
mixins: [CreateEditView, VM_MIXIN],
@@ -95,6 +97,10 @@ export default {
secretNamePrefix() {
return this.templateValue?.metadata?.name;
},
filesystemEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('supportFilesystem');
},
},
watch: {
@@ -154,6 +160,7 @@ export default {
mounted() {
this.imageId = this.diskRows[0]?.image || '';
this['filesystemRows'] = this.getFilesystemRows(this.value.spec.vm);
},
methods: {
@@ -349,6 +356,19 @@ export default {
</template>
</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
name="labels"
:label="t('generic.labels')"
@@ -2,6 +2,7 @@
import Footer from '@shell/components/form/Footer';
import { RadioGroup } from '@components/Form/Radio';
import { LabeledInput } from '@components/Form/LabeledInput';
import Checkbox from '@components/Form/Checkbox/Checkbox';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
@@ -32,6 +33,7 @@ const createObject = {
export default {
name: 'CreateRestore',
components: {
Checkbox,
Footer,
RadioGroup,
LabeledInput,
@@ -249,9 +251,17 @@ export default {
<LabeledSelect
v-if="!restoreNewVm"
v-model:value="deletionPolicy"
class="mb-20"
:label="t('harvester.backup.restore.deletePreviousVolumes')"
:options="deletionPolicyOption"
/>
<Checkbox
v-model:value="restoreResource.spec.haltAfterRestore"
type="checkbox"
class="check mb-20"
:label="t('harvester.backup.restore.haltAfterRestore')"
/>
</div>
<Footer
+27 -7
View File
@@ -43,18 +43,20 @@ export default {
created() {
const vpc = this.$route.query.vpc || '';
const enableDHCP = this.value?.spec?.enableDHCP || false;
const natOutgoing = this.value?.spec?.natOutgoing || false;
set(this.value.spec, 'enableDHCP', enableDHCP);
set(this.value, 'spec', this.value.spec || {
cidrBlock: '',
protocol: NETWORK_PROTOCOL.IPv4,
provider: '',
cidrBlock: '',
protocol: NETWORK_PROTOCOL.IPv4,
provider: '',
vpc,
gatewayIP: '',
excludeIps: [],
private: false,
gateway: '',
excludeIps: [],
private: false,
enableDHCP,
acls: []
natOutgoing,
acls: []
});
},
@@ -129,6 +131,10 @@ export default {
label: 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);
}
},
@@ -304,6 +310,20 @@ export default {
</Banner>
</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="col span-6">
<RadioGroup
@@ -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"
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>
@@ -152,6 +152,10 @@ export default {
return !rows.find((device) => !device.passthroughClaim);
},
canManageGroup(rows = []) {
return rows.length > 0 && rows.every((row) => row.canUpdate === true);
},
changeRows(filterRows, parentSriov) {
this['filterRows'] = filterRows;
this['parentSriov'] = parentSriov;
@@ -185,22 +189,27 @@ export default {
v-trim-whitespace
class="group-tab"
>
<button
v-if="groupIsAllEnabled(group.rows)"
type="button"
class="btn btn-sm role-secondary mr-5"
@click="e=>{disableGroup(group.rows); e.target.blur()}"
<div
v-if="canManageGroup(group.rows)"
class="group-actions"
>
{{ t('harvester.pci.disableGroup') }}
</button>
<button
v-else
type="button"
class="btn btn-sm role-secondary mr-5"
@click="e=>{enableGroup(group.rows); e.target.blur()}"
>
{{ t('harvester.pci.enableGroup') }}
</button>
<button
v-if="groupIsAllEnabled(group.rows)"
type="button"
class="btn btn-sm role-secondary mr-5"
@click="e=>{disableGroup(group.rows); e.target.blur()}"
>
{{ t('harvester.pci.disableGroup') }}
</button>
<button
v-else
type="button"
class="btn btn-sm role-secondary mr-5"
@click="e=>{enableGroup(group.rows); e.target.blur()}"
>
{{ t('harvester.pci.enableGroup') }}
</button>
</div>
<span v-clean-html="group.key" />
</div>
</template>
@@ -232,3 +241,9 @@ export default {
</template>
</ResourceTable>
</template>
<style lang="scss" scoped>
.group-actions {
display: inline;
}
</style>
@@ -52,31 +52,21 @@ export default {
}
const selectedDevices = [];
const oldFormatDevices = [];
const vmDevices = this.value?.domain?.devices?.hostDevices || [];
const otherDevices = this.otherDevices(vmDevices).map(({ name }) => name);
const vmDeviceNames = vmDevices.map(({ name }) => name);
this.pciDevices.forEach((row) => {
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
});
vmDevices.forEach(({ name, deviceName }) => {
const checkName = (deviceName || '').split('/')?.[1];
if (checkName && name.includes(checkName) && !otherDevices.includes(name)) {
oldFormatDevices.push(name);
} else if (this.enabledDevices.find((device) => device?.metadata?.name === name)) {
vmDevices.forEach(({ name }) => {
if (this.enabledDevices.find((device) => device?.metadata?.name === name)) {
selectedDevices.push(name);
}
});
if (oldFormatDevices.length > 0) {
this.oldFormatDevices = oldFormatDevices;
} else {
this.selectedDevices = selectedDevices;
}
this.selectedDevices = selectedDevices;
},
data() {
@@ -87,7 +77,6 @@ export default {
selectedDevices: [],
pciDeviceSchema: this.$store.getters['harvester/schemaFor'](HCI.PCI_DEVICE),
showMatrix: false,
oldFormatDevices: [],
};
},
@@ -199,11 +188,6 @@ export default {
});
},
oldFormatDevicesHTML() {
return this.oldFormatDevices.map((device) => {
return `<li>${ device }</li>`;
}).join('');
},
},
methods: {
@@ -227,101 +211,89 @@ export default {
<template>
<div>
<div
v-if="oldFormatDevices.length > 0"
class="row"
>
<div class="row">
<div class="col span-12">
<Banner color="warning">
<p v-clean-html="t('harvester.pci.oldFormatDevices.help', {oldFormatDevicesHTML}, true)" />
<Banner color="info">
<MessageLink
:to="toVGpuDevicesPage"
prefix-label="harvester.pci.howToUseDeviceInVMCreation.prefix"
middle-label="harvester.pci.howToUseDeviceInVMCreation.middle"
suffix-label="harvester.pci.howToUseDeviceInVMCreation.suffix"
/>
</Banner>
<Banner
v-if="selectedDevices.length > 0"
color="info"
>
<t k="harvester.pci.deviceInTheSameHost" />
</Banner>
</div>
</div>
<div v-else>
<template v-if="enabledDevices.length">
<div class="row">
<div class="col span-12">
<Banner color="info">
<MessageLink
:to="toVGpuDevicesPage"
prefix-label="harvester.pci.howToUseDeviceInVMCreation.prefix"
middle-label="harvester.pci.howToUseDeviceInVMCreation.middle"
suffix-label="harvester.pci.howToUseDeviceInVMCreation.suffix"
/>
</Banner>
<Banner
v-if="selectedDevices.length > 0"
color="info"
<div class="col span-6">
<LabeledSelect
v-model:value="selectedDevices"
label="Available PCI Devices"
searchable
multiple
taggable
:options="deviceOpts"
:mode="mode"
>
<t k="harvester.pci.deviceInTheSameHost" />
</Banner>
<template #option="option">
<span>{{ option.value }} <span class="text-label">({{ option.displayLabel }})</span></span>
</template>
</LabeledSelect>
</div>
</div>
<template v-if="enabledDevices.length">
<div class="row">
<div class="col span-6">
<LabeledSelect
v-model:value="selectedDevices"
label="Available PCI Devices"
searchable
multiple
taggable
:options="deviceOpts"
:mode="mode"
>
<template #option="option">
<span>{{ option.value }} <span class="text-label">({{ option.displayLabel }})</span></span>
</template>
</LabeledSelect>
</div>
<div
v-if="compatibleNodes.length && selectedDevices.length"
class="row"
>
<div class="col span-12 text-muted">
Compatible hosts:
<!-- eslint-disable-next-line vue/no-parsing-error -->
<span
v-for="(node, idx) in compatibleNodes"
:key="idx"
>{{ node }}{{ idx < compatibleNodes.length-1 ? ', ' : '' }}</span>
</div>
<div
v-if="compatibleNodes.length && selectedDevices.length"
class="row"
>
<div class="col span-12 text-muted">
Compatible hosts:
<!-- eslint-disable-next-line vue/no-parsing-error -->
<span
v-for="(node, idx) in compatibleNodes"
:key="idx"
>{{ node }}{{ idx < compatibleNodes.length-1 ? ', ' : '' }}</span>
</div>
</div>
<div
v-else-if="selectedDevices.length"
class="text-error"
>
{{ t('harvester.pci.impossibleSelection') }}
</div>
<button
type="button"
class="btn btn-sm role-link pl-0"
@click="e=>{showMatrix = !showMatrix; e.target.blur()}"
>
{{ showMatrix ? t('harvester.pci.hideCompatibility') : t('harvester.pci.showCompatibility') }}
</button>
<div
v-if="showMatrix"
class="row mt-20"
>
<div class="col span-12">
<CompatibilityMatrix
:enabled-devices="enabledDevices"
:devices-by-node="devicesByNode"
:devices-in-use="devicesInUse"
/>
</div>
</div>
</template>
<div class="row mt-20">
</div>
<div
v-else-if="selectedDevices.length"
class="text-error"
>
{{ t('harvester.pci.impossibleSelection') }}
</div>
<button
type="button"
class="btn btn-sm role-link pl-0"
@click="e=>{showMatrix = !showMatrix; e.target.blur()}"
>
{{ showMatrix ? t('harvester.pci.hideCompatibility') : t('harvester.pci.showCompatibility') }}
</button>
<div
v-if="showMatrix"
class="row mt-20"
>
<div class="col span-12">
<DeviceList
:schema="pciDeviceSchema"
:devices="pciDevices"
@submit.prevent
<CompatibilityMatrix
:enabled-devices="enabledDevices"
:devices-by-node="devicesByNode"
:devices-in-use="devicesInUse"
/>
</div>
</div>
</template>
<div class="row mt-20">
<div class="col span-12">
<DeviceList
:schema="pciDeviceSchema"
:devices="pciDevices"
@submit.prevent
/>
</div>
</div>
</div>
</template>
@@ -10,6 +10,7 @@ import { _VIEW } from '@shell/config/query-params';
import { NAMESPACE } from '@shell/config/types';
import { HCI } from '../../types';
import { getLoginAwareErrors } from '../../utils/error';
const _NEW = '_NEW';
@@ -214,7 +215,9 @@ export default {
buttonCb(true);
this.cancel();
} catch (err) {
this.errors = [err.message];
const message = this.t('harvester.virtualMachine.genericLoginError');
this.errors = getLoginAwareErrors(err, message);
buttonCb(false);
}
},
@@ -64,6 +64,12 @@ export default {
value: 'status.productID',
sort: ['status.productID', 'status.vendorID']
},
{
name: 'classType',
labelKey: 'harvester.usb.classType',
value: 'status.classType',
sort: ['status.classType']
},
];
if (!isSingleProduct) {
@@ -107,6 +113,11 @@ export default {
}
});
},
canManageGroup(rows = []) {
return rows.every((row) => row.canUpdate === true);
},
groupIsAllEnabled(rows = []) {
return !rows.find((device) => !device.passthroughClaim);
},
@@ -147,22 +158,27 @@ export default {
v-trim-whitespace
class="group-tab"
>
<button
v-if="groupIsAllEnabled(group.rows)"
type="button"
class="btn btn-sm role-secondary mr-5"
@click="e=>{disableGroup(group.rows); e.target.blur()}"
<div
v-if="canManageGroup(group.rows)"
class="group-actions"
>
{{ t('harvester.usb.disableGroup') }}
</button>
<button
v-else
type="button"
class="btn btn-sm role-secondary mr-5"
@click="e=>{enableGroup(group.rows); e.target.blur()}"
>
{{ t('harvester.usb.enableGroup') }}
</button>
<button
v-if="groupIsAllEnabled(group.rows)"
type="button"
class="btn btn-sm role-secondary mr-5"
@click="e=>{disableGroup(group.rows); e.target.blur()}"
>
{{ t('harvester.usb.disableGroup') }}
</button>
<button
v-else
type="button"
class="btn btn-sm role-secondary mr-5"
@click="e=>{enableGroup(group.rows); e.target.blur()}"
>
{{ t('harvester.usb.enableGroup') }}
</button>
</div>
<span v-clean-html="group.key" />
</div>
</template>
@@ -175,3 +191,9 @@ export default {
</template>
</ResourceTable>
</template>
<style lang="scss" scoped>
.group-actions {
display: inline;
}
</style>
@@ -475,7 +475,6 @@ export default {
<button
type="button"
class="btn btn-sm bg-primary mr-15 mb-10"
:disabled="rows.length === 0"
@click="addVolume(SOURCE_TYPE.NEW)"
>
{{ t('harvester.virtualMachine.volume.addVolume') }}
@@ -37,6 +37,7 @@ import Network from './VirtualMachineNetwork';
import Volume from './VirtualMachineVolume';
import SSHKey from './VirtualMachineSSHKey';
import Reserved from './VirtualMachineReserved';
import Filesystem from './VirtualMachineFilesystem';
import { Banner } from '@components/Banner';
import MessageLink from '@shell/components/MessageLink';
@@ -72,6 +73,7 @@ export default {
Banner,
MessageLink,
UsbDevices,
Filesystem,
},
mixins: [CreateEditView, VM_MIXIN],
@@ -91,22 +93,36 @@ export default {
const hostname = this.value.spec.template.spec.hostname || '';
const customizeDisplayName = !!(this.value.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME]);
return {
cloneVM,
count: 2,
templateId: '',
templateVersionId: '',
namePrefix: '',
isSingle: true,
isOpen: false,
count: 2,
templateId: '',
templateVersionId: '',
namePrefix: '',
isSingle: true,
isOpen: false,
hostname,
isRestartImmediately,
customizeDisplayName,
};
},
computed: {
...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() {
return {
name: 'harvester-c-cluster-resource',
@@ -218,6 +234,9 @@ export default {
usbPassthroughEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
},
filesystemEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('supportFilesystem');
},
},
watch: {
@@ -289,6 +308,12 @@ export default {
this.getInitConfig({ value: this.value, init: this.isCreate });
}
},
customizeDisplayName(neu) {
if (!neu) {
this.value.setAnnotation(HCI_ANNOTATIONS.VM_DISPLAY_NAME, '');
}
},
},
created() {
@@ -321,6 +346,7 @@ export default {
const diskRows = this.getDiskRows(this.value);
this['diskRows'] = diskRows;
this['filesystemRows'] = this.getFilesystemRows(this.value);
const templateId = this.$route.query.templateId;
const templateVersionId = this.$route.query.versionId;
@@ -610,6 +636,33 @@ export default {
</template>
</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
v-if="isCreate"
v-model:value="useTemplate"
@@ -783,10 +836,23 @@ export default {
/>
</Tab>
<Tab
v-if="filesystemEnabled"
name="filesystem"
:label="t('harvester.tab.filesystem')"
:weight="-9"
>
<Filesystem
v-model:value="filesystemRows"
:mode="mode"
:namespace="value.metadata.namespace"
/>
</Tab>
<Tab
name="labels"
:label="t('generic.labels')"
:weight="-9"
:weight="-10"
>
<Banner color="info">
<t k="harvester.virtualMachine.labels.banner" />
@@ -805,7 +871,7 @@ export default {
<Tab
name="instanceLabel"
:label="t('harvester.tab.instanceLabel')"
:weight="-10"
:weight="-11"
>
<Banner color="info">
<t k="harvester.virtualMachine.instanceLabels.banner" />
@@ -826,7 +892,7 @@ export default {
<Tab
name="annotations"
:label="t('harvester.tab.annotations')"
:weight="-11"
:weight="-12"
>
<Banner color="info">
<t k="harvester.virtualMachine.annotations.banner" />
@@ -847,7 +913,7 @@ export default {
<Tab
name="advanced"
:label="t('harvester.tab.advanced')"
:weight="-12"
:weight="-13"
>
<div class="row mb-20">
<div class="col span-6">
@@ -20,12 +20,7 @@ export default {
computed: {
vmiResource() {
const vmiList = this.$store.getters['harvester/all'](HCI.VMI) || [];
const vmi = vmiList.find( (VMI) => {
return VMI?.metadata?.ownerReferences?.[0]?.uid === this.vmResource?.metadata?.uid;
});
return vmi;
return this.$store.getters['harvester/byId'](HCI.VMI, this.vmResource?.id) || null;
},
migrationState() {
return this.vmiResource?.migrationState?.status || '';
@@ -14,20 +14,6 @@ export default {
type: Object,
required: true
},
allNodeNetwork: {
type: Array,
default: () => {
return [];
}
},
allClusterNetwork: {
type: Array,
default: () => {
return [];
}
}
},
data() {
+42 -20
View File
@@ -144,6 +144,10 @@ harvester:
migration:
failedMessage: Latest migration failed!
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:
nodeName:
label: Target Node
@@ -248,6 +252,7 @@ harvester:
suspendSchedule: Suspend
restoreExistingVM: Replace Existing
migrate: Migrate
vmMigrate: Virtual Machine Migration
cpuAndMemoryHotplug: Edit CPU and Memory
abortMigration: Abort Migration
storageMigration: Storage Migration
@@ -298,6 +303,7 @@ harvester:
phase: Phase
attachedVM: Attached Virtual Machine
cpuManager: CPU Manager
routeConnectivityTooltip: Connectivity between the VM network and the management network, which the Harvester nodes are connected to.
fingerprint: Fingerprint
value: Value
actions: Actions
@@ -349,6 +355,7 @@ harvester:
snapshots: Snapshots
instanceLabel: Instance Labels
annotations: Annotations
filesystem: Filesystem Volume
fields:
version: Version
name: Name
@@ -385,21 +392,6 @@ harvester:
middle: vGPU Devices
suffix: page.
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
hideCompatibility: Hide device compatibility matrix
claimError: Error enabling passthrough on {name}
@@ -417,6 +409,8 @@ harvester:
suffix: to enable the add-on to successfully manage your PCI devices.
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.
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:
matrixHostName: Host Name
@@ -726,6 +720,7 @@ harvester:
other {Start}
} Now
createSSHKey: Create a New...
genericLoginError: Authentication failed. Please re-log in and try again.
installAgent: Install guest agent
enableUsb: Enable USB Tablet
advancedOptions:
@@ -823,6 +818,18 @@ harvester:
username: Username
password: Password
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 ?'
detail:
tabs:
@@ -1111,6 +1118,7 @@ harvester:
replaceExisting: Replace existing
virtualMachineName: Virtual Machine Name
keepMacAddress: Keep MAC Address
haltAfterRestore: Keep powered off after restore
matchTarget: The current backup target does not match the existing one.
progress:
details: Volume details
@@ -1148,6 +1156,9 @@ harvester:
placeholder: key1=value1, key2=value2
dhcpOptionBanner: DHCP options is a key/value string concatenate with comma. For more detail, please refer to <a href="https://kubeovn.github.io/docs/v1.13.x/en/kubevirt/dhcp/" target="_blank">KubOVN document</a>
tooltip: Enable DHCP server for this subnet. When enabled, VMs can automatically obtain IP addresses from this subnet.
externalConnectivity:
label: NAT Outgoing
tooltip: Enable NAT for VMs using this subnet to access external networks.
private:
label: Private Subnet
tooltip: Enable network isolation for this Subnet. When enabled, VMs can only communicate within this subnet, even if other subnets exist under the same VPC.
@@ -1285,6 +1296,13 @@ harvester:
rancherCluster:
kubeConfig: Rancher KubeConfig
removeUpstreamClusterWhenNamespaceIsDeleted: Remove Upstream Cluster When Namespace Is Deleted
clusterPodSecurityStandard:
whitelistedNamespaces:
label: 'Whitelisted Namespaces'
privilegedNamespaces:
label: 'Privileged Namespaces'
restrictedNamespaces:
label: 'Restricted Namespaces'
storageNetwork:
range:
placeholder: e.g. 172.16.0.0/24
@@ -1508,6 +1526,7 @@ harvester:
vmSnapshot:
label: Virtual Machine Snapshots
createText: Restore Snapshot
title: Restore Virtual Machine
snapshot: Snapshot
storage:
@@ -1515,6 +1534,7 @@ harvester:
useDefault: Use the default storage
volumeEncryption: Volume Encryption
secret: Secret
volumeExpansionCheckbox: Enable Expansion
migratable:
label: Migratable
numberOfReplicas:
@@ -1764,6 +1784,8 @@ harvester:
repository: Image Repository
driver:
location: Driver Location
enable:
title: Enable NVIDIA Driver Toolkit
parsingSpecError:
The field 'spec.valuesContent' has invalid format.
usbController:
@@ -1893,6 +1915,8 @@ harvester:
migconfiguration:
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.
profileSpec: Profile Specs
profileStatus: Profile Status
@@ -1955,6 +1979,7 @@ harvester:
title: Cannot Disable 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.'
classType: Class Type
harvesterVlanConfigMigrateDialog:
targetClusterNetwork:
@@ -2039,6 +2064,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-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-cluster-pod-security-standard': 'Enforce Kubernetes Pod Security Standards (PSS) at the cluster level.'
typeLabel:
kubevirt.io.virtualmachine: |-
@@ -2161,11 +2187,7 @@ typeLabel:
one { PCI Device }
other { PCI Devices }
}
persistentvolumeclaim: |-
{count, plural,
one { Volume }
other { Volumes }
}
network.harvesterhci.io.clusternetwork: |-
{count, plural,
one { Cluster Network }
@@ -3,7 +3,6 @@ import { Banner } from '@components/Banner';
import Loading from '@shell/components/Loading';
import ResourceTable from '@shell/components/ResourceTable';
import BadgeState from '@shell/components/formatter/BadgeStateFormatter';
import { NAME, AGE, NAMESPACE, STATE } from '@shell/config/table-headers';
import { NETWORK_ATTACHMENT, SCHEMA } from '@shell/config/types';
import { allHash } from '@shell/utils/promise';
@@ -113,6 +112,7 @@ export default {
value: 'connectivity',
labelKey: 'tableHeaders.routeConnectivity',
formatter: 'NetworkRouteConnectivity',
tooltip: 'harvester.tableHeaders.routeConnectivityTooltip',
formatterOpts: { arbitrary: true },
width: 130,
},
@@ -12,11 +12,18 @@ import { HCI } from '../types';
import HarvesterVmState from '../formatters/HarvesterVmState';
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 = [
STATE,
{
...NAME,
width: 350,
value: 'nameDisplay',
sort: ['nameDisplay'],
},
NAMESPACE,
{
@@ -93,19 +100,9 @@ export default {
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);
this.allVMs = hash.vms;
this.allNodeNetworks = hash.nodeNetworks || [];
this.allClusterNetworks = hash.clusterNetworks || [];
},
data() {
@@ -113,8 +110,6 @@ export default {
hasNode: false,
allVMs: [],
allVMIs: [],
allNodeNetworks: [],
allClusterNetworks: [],
restartNotificationDisplayed: false,
HCI
};
@@ -163,6 +158,12 @@ export default {
*/
hasBackUpRestoreInProgress() {
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,53 +182,35 @@ export default {
},
watch: {
allVMs: {
handler(neu) {
const vmNames = [];
vmRestartRequiredNames(vmNames) {
const count = vmNames.length;
neu.forEach((vm) => {
if (vm.isRestartRequired) {
vmNames.push(vm.metadata.name);
}
});
const count = vmNames.length;
if (count === 0 && this.restartNotificationDisplayed) {
this.restartNotificationDisplayed = false;
if ( count === 0 && this.restartNotificationDisplayed) {
this.restartNotificationDisplayed = false;
return;
}
return;
if (count > 0) {
// clear old notification before showing new one
if (this.restartNotificationDisplayed) {
this.$store.dispatch('growl/clear');
}
if (count > 0) {
// clear old notification before showing new one
if (this.restartNotificationDisplayed) {
this.$store.dispatch('growl/clear');
}
}
if (count > 0 && vmNames.length > 0) {
this.$store.dispatch('growl/warning', {
title: this.t('harvester.notification.restartRequired.title', { count }),
message: this.t('harvester.notification.restartRequired.message', { vmNames: vmNames.join(', ') }),
timeout: 10000,
}, { root: true });
this.restartNotificationDisplayed = true;
}
},
deep: true,
this.$store.dispatch('growl/warning', {
title: this.t('harvester.notification.restartRequired.title', { count }),
message: this.t('harvester.notification.restartRequired.message', { vmNames: vmNames.join(', ') }),
timeout: 10000,
}, { root: true });
this.restartNotificationDisplayed = true;
}
}
},
methods: {
lockIconTooltipMessage(row) {
const message = '';
const key = ENCRYPTED_VOLUME_TOOLTIP_KEYS[row.encryptedVolumeType];
if (row.encryptedVolumeType === 'all') {
return this.t('harvester.virtualMachine.volume.lockTooltip.all');
} else if (row.encryptedVolumeType === 'partial') {
return this.t('harvester.virtualMachine.volume.lockTooltip.partial');
}
return message;
return key ? this.t(key) : '';
}
}
};
@@ -253,8 +236,6 @@ export default {
<HarvesterVmState
class="vmstate"
:row="scope.row"
:all-node-network="allNodeNetworks"
:all-cluster-network="allClusterNetworks"
/>
</div>
</template>
@@ -265,16 +246,16 @@ export default {
v-if="scope.row.type !== HCI.VMI"
:to="scope.row.detailLocation"
>
{{ scope.row.metadata.name }}
{{ scope.row.nameDisplay }}
<i
v-if="lockIconTooltipMessage(scope.row)"
v-if="scope.row.encryptedVolumeType !== 'none'"
v-tooltip="lockIconTooltipMessage(scope.row)"
class="icon icon-lock"
:class="{'green-icon': scope.row.encryptedVolumeType === 'all', 'yellow-icon': scope.row.encryptedVolumeType === 'partial'}"
/>
</router-link>
<span v-else>
{{ scope.row.metadata.name }}
{{ scope.row.nameDisplay }}
</span>
<ConsoleBar
:resource-type="scope.row"
+92 -6
View File
@@ -12,7 +12,7 @@ import { base64Decode } from '@shell/utils/crypto';
import { formatSi, parseSi } from '@shell/utils/units';
import { _CLONE, _CREATE, _VIEW } from '@shell/config/query-params';
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';
import { HOSTNAME } from '@shell/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 impl, { QGA_JSON, USB_TABLET } from './impl';
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';
@@ -102,6 +102,8 @@ export default {
vmims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIM }),
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
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 }),
longhornV2Engine: this.$store.dispatch(`${ inStore }/find`, { type: LONGHORN.SETTINGS, id: LONGHORN_V2_DATA_ENGINE }),
};
@@ -156,6 +158,7 @@ export default {
imageId: '',
diskRows: [],
networkRows: [],
filesystemRows: [],
machineType: '',
machineTypes: [],
secretName: '',
@@ -440,6 +443,7 @@ export default {
this['imageId'] = imageId;
this['diskRows'] = diskRows;
this['filesystemRows'] = this.getFilesystemRows(vm);
this.refreshYamlEditor();
},
@@ -641,6 +645,80 @@ export default {
this.parseAccessCredentials();
this.parseNetworkRows(this.networkRows);
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() {
@@ -907,14 +985,22 @@ export default {
const specInterfaces = this.spec?.template?.spec?.domain?.devices?.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 = {
...this.spec.template.spec,
domain: {
...this.spec.template.spec.domain,
devices: {
...this.spec.template.spec.domain.devices,
interfaces: mergedInterfaces,
},
devices,
},
networks
};
@@ -9,6 +9,7 @@ import HarvesterResource from './harvester';
export default class MIGCONFIGURATION extends HarvesterResource {
get _availableActions() {
let out = super._availableActions;
const canUpdate = !!this.linkFor('update');
out = out.map((action) => {
if (action.action === 'showConfiguration') {
@@ -26,13 +27,13 @@ export default class MIGCONFIGURATION extends HarvesterResource {
out.push(
{
action: 'enableConfig',
enabled: !this.isEnabled,
enabled: !this.isEnabled && canUpdate,
icon: 'icon icon-fw icon-dot',
label: 'Enable',
},
{
action: 'disableConfig',
enabled: this.isEnabled,
enabled: this.isEnabled && canUpdate,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable',
},
@@ -62,13 +63,27 @@ export default class MIGCONFIGURATION extends HarvesterResource {
}
get stateDisplay() {
if (this.configStatus === 'out-of-sync') {
return this.t('harvester.migconfiguration.status.outOfSync');
}
return this.actualState;
}
get stateColor() {
const state = this.actualState;
get stateDescription() {
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() {
@@ -34,7 +34,7 @@ export default class PCIDevice extends SteveModel {
out.push(
{
action: 'enablePassthroughBulk',
enabled: !this.isEnabling && !this.isvGPUDevice,
enabled: !this.isEnabling && !this.isvGPUDevice && this.canUpdate,
icon: 'icon icon-fw icon-dot',
label: 'Enable Passthrough',
bulkable: true,
@@ -43,7 +43,7 @@ export default class PCIDevice extends SteveModel {
},
{
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',
label: 'Disable Passthrough',
bulkable: true,
@@ -54,6 +54,10 @@ export default class PCIDevice extends SteveModel {
return out;
}
get canUpdate() {
return !!this.linkFor('update');
}
get isvGPUDevice() {
if (!this.vGPUAsPCIDeviceFeatureEnabled) {
return false;
@@ -16,13 +16,13 @@ export default class SRIOVDevice extends SteveModel {
out.push(
{
action: 'enableDevice',
enabled: !this.isEnabled,
enabled: !this.isEnabled && this.canUpdate,
icon: 'icon icon-fw icon-dot',
label: 'Enable',
},
{
action: 'disableDevice',
enabled: this.isEnabled,
enabled: this.isEnabled && this.canUpdate,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable',
},
@@ -31,6 +31,10 @@ export default class SRIOVDevice extends SteveModel {
return out;
}
get canUpdate() {
return !!this.linkFor('update');
}
get canYaml() {
return false;
}
@@ -33,7 +33,7 @@ export default class USBDevice extends SteveModel {
out.push(
{
action: 'enablePassthroughBulk',
enabled: !this.passthroughClaim && !this.status.enabled,
enabled: !this.passthroughClaim && !this.status.enabled && this.canUpdate,
icon: 'icon icon-fw icon-dot',
label: 'Enable Passthrough',
bulkable: true,
@@ -42,7 +42,7 @@ export default class USBDevice extends SteveModel {
},
{
action: 'disablePassthrough',
enabled: this.status.enabled,
enabled: this.status.enabled && this.canUpdate,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable Passthrough',
bulkable: true,
@@ -53,6 +53,10 @@ export default class USBDevice extends SteveModel {
return out;
}
get canUpdate() {
return !!this.linkFor('update');
}
get canYaml() {
return false;
}
@@ -27,17 +27,18 @@ const STATUS_DISPLAY = {
export default class VGpuDevice extends SteveModel {
get _availableActions() {
const out = super._availableActions;
const canUpdate = !!this.linkFor('update');
out.push(
{
action: 'enableVGpu',
enabled: !this.isEnabled,
enabled: !this.isEnabled && canUpdate,
icon: 'icon icon-fw icon-dot',
label: 'Enable',
},
{
action: 'disableVGpu',
enabled: this.isEnabled,
enabled: this.isEnabled && canUpdate,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable',
bulkable: true,
+11 -1
View File
@@ -24,7 +24,7 @@ const OBSCURE_NAMESPACE_PREFIX = [
export default class HciNamespace extends namespace {
get _availableActions() {
const out = super._availableActions;
let out = super._availableActions;
const remove = out.findIndex((a) => a.action === 'promptRemove');
const promptRemove = {
@@ -53,6 +53,16 @@ export default class HciNamespace extends namespace {
insertAt(out, out.length - 1, promptRemove);
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;
}
+14
View File
@@ -5,6 +5,20 @@ import Secret from '@shell/models/secret';
import { NAMESPACE } from '@shell/config/types';
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.
// See isFullPageOverride in https://github.com/rancher/dashboard/blob/master/shell/components/ResourceDetail/index.vue
get fullDetailPageOverride() {
@@ -96,6 +96,10 @@ export default class HciStorageClass extends StorageClass {
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
}
get expandOnlineEncryptedVolumeFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('expandOnlineEncryptedVolume');
}
get thirdPartyStorageFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
}
@@ -106,6 +110,15 @@ export default class HciStorageClass extends StorageClass {
get 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()) {
out = out.filter((action) => {
+13 -1
View File
@@ -4,6 +4,8 @@ import { HCI as HCI_ANNOTATIONS } from '../config/labels-annotations';
import HarvesterResource from './harvester';
import { HCI } from '../types';
const HARVESTER_NVIDIA_DRIVER_TOOLKIT = 'harvester-system/nvidia-driver-toolkit';
export default class HciAddonConfig extends HarvesterResource {
get availableActions() {
const out = super._availableActions;
@@ -19,9 +21,10 @@ export default class HciAddonConfig extends HarvesterResource {
out.push(rancherDashboard);
}
const canUpdate = !!this.linkFor('update');
const toggleAddon = {
action: 'toggleAddon',
enabled: true,
enabled: canUpdate,
icon: this.spec.enabled ? 'icon icon-pause' : 'icon icon-play',
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;
await this.save();
} catch (err) {
@@ -3,6 +3,20 @@ import { findBy } from '@shell/utils/array';
import HarvesterResource from './harvester';
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() {
const conditions = get(this, 'status.conditions');
const status = (findBy(conditions, 'type', 'validated') || {}).status ;
@@ -19,16 +19,18 @@ export default class ScheduleVmBackup extends HarvesterResource {
}
});
const canUpdate = !!this.linkFor('update');
return [
{
action: 'resumeSchedule',
enabled: ucFirst(this.state) === STATES.suspended.label,
enabled: canUpdate && ucFirst(this.state) === STATES.suspended.label,
icon: 'icons icon-play',
label: this.t('harvester.action.resumeSchedule'),
},
{
action: 'suspendSchedule',
enabled: ucFirst(this.state) === STATES.active.label,
enabled: canUpdate && ucFirst(this.state) === STATES.active.label,
icon: 'icons icon-pause',
label: this.t('harvester.action.suspendSchedule'),
},
@@ -39,9 +39,6 @@ function isReady() {
export default class HciVmImage extends HarvesterResource {
get availableActions() {
let out = super._availableActions;
const toFilter = ['goToEditYaml'];
out = out.filter( (A) => !toFilter.includes(A.action));
// show `Clone` only when imageSource is `download`
if (this.imageSource !== 'download') {
@@ -55,6 +52,7 @@ export default class HciVmImage extends HarvesterResource {
canCreateVM = false;
}
const canCreateImage = !!this.$getters?.['schemaFor']?.(HCI.IMAGE)?.collectionMethods?.some((method) => method.toLowerCase() === 'post');
const customActions = this.isReady ? [
{
action: 'createFromImage',
@@ -64,13 +62,13 @@ export default class HciVmImage extends HarvesterResource {
},
{
action: 'encryptImage',
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted,
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted && canCreateImage,
icon: 'icon icon-lock',
label: this.t('harvester.action.encryptImage'),
},
{
action: 'decryptImage',
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted,
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted && canCreateImage,
icon: 'icon icon-unlock',
label: this.t('harvester.action.decryptImage'),
},
@@ -23,17 +23,17 @@ export default class HciVmTemplateVersion extends HarvesterResource {
});
const schema = this.$getters['schemaFor'](HCI.VM);
let canCreateVM = true;
let canCreateVM = false;
if ( schema && !schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())) ) {
canCreateVM = false;
if (schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())) ) {
canCreateVM = true;
}
return [
{
action: 'launchFromTemplate',
icon: 'icon icon-spinner',
disabled: !canCreateVM || !this.isReady,
enabled: canCreateVM && this.isReady,
label: this.t('harvester.action.launchFormTemplate'),
},
{
@@ -83,6 +83,42 @@ const VMIPhase = {
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'];
export default class VirtVm extends HarvesterResource {
@@ -94,6 +130,8 @@ export default class VirtVm extends HarvesterResource {
clone.action = 'goToCloneVM';
}
const canCreateVMSSchedule = !!this.$getters?.['schemaFor']?.(HCI.SCHEDULE_VM_BACKUP)?.collectionMethods?.find((x) => ['post'].includes(x.toLowerCase()));
return [
{
action: 'stopVM',
@@ -126,6 +164,7 @@ export default class VirtVm extends HarvesterResource {
},
{
action: 'restartVM',
altAction: 'altRestartVM',
enabled: !!this.actions?.restart,
icon: 'icon icon-refresh',
label: this.t('harvester.action.restart'),
@@ -133,10 +172,11 @@ export default class VirtVm extends HarvesterResource {
bulkAction: 'restartVM'
},
{
action: 'softrebootVM',
enabled: !!this.actions?.softreboot,
icon: 'icon icon-pipeline',
label: this.t('harvester.action.softreboot')
action: 'softrebootVM',
altAction: 'doSoftReboot',
enabled: !!this.actions?.softreboot,
icon: 'icon icon-pipeline',
label: this.t('harvester.action.softreboot')
},
{
action: 'startVM',
@@ -171,7 +211,7 @@ export default class VirtVm extends HarvesterResource {
},
{
action: 'createSchedule',
enabled: this.schedulingVMBackupFeatureEnabled,
enabled: canCreateVMSSchedule && this.schedulingVMBackupFeatureEnabled,
icon: 'icon icon-history',
label: this.t('harvester.action.createSchedule')
},
@@ -188,10 +228,12 @@ export default class VirtVm extends HarvesterResource {
label: this.t('harvester.action.ejectCDROM')
},
{
action: 'migrateVM',
enabled: !!this.actions?.migrate,
icon: 'icon icon-copy',
label: this.t('harvester.action.migrate')
action: 'migrateVM',
enabled: !!this.actions?.migrate,
icon: 'icon icon-copy',
label: this.t('harvester.action.vmMigrate'),
bulkable: true,
bulkAction: 'migrateVM'
},
{
action: 'abortMigrationVM',
@@ -331,6 +373,10 @@ export default class VirtVm extends HarvesterResource {
this.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
}
altRestartVM() {
this.doActionGrowl('restart', {});
}
restartVM(resources = this) {
this.$dispatch('promptModal', {
resources,
@@ -660,16 +706,13 @@ export default class VirtVm extends HarvesterResource {
get podResource() {
const inStore = this.productInStore;
const vmiResource = this.$rootGetters[`${ inStore }/byId`](HCI.VMI, this.id);
const podList = this.$rootGetters[`${ inStore }/all`](POD);
return podList.find((P) => {
return (
vmiResource?.metadata?.name &&
vmiResource?.metadata?.name === P.metadata?.ownerReferences?.[0].name
);
});
if (!vmiResource?.metadata?.name) {
return undefined;
}
return getPodByOwnerName(this.$rootGetters, inStore, vmiResource.metadata.name);
}
get isPaused() {
@@ -710,17 +753,13 @@ export default class VirtVm extends HarvesterResource {
get vmi() {
const inStore = this.productInStore;
const vmis = this.$rootGetters[`${ inStore }/all`](HCI.VMI);
return vmis.find((VMI) => VMI.id === this.id);
return this.$rootGetters[`${ inStore }/byId`](HCI.VMI, this.id);
}
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) || [];
return pvcs.filter((pvc) => volumeClaimNames.includes(pvc.metadata.name));
return getPvcsByNames(this.$rootGetters, this.productInStore, volumeClaimNames);
}
get lvmVolumes() {
@@ -753,17 +792,6 @@ export default class VirtVm extends HarvesterResource {
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;
}
@@ -901,9 +929,7 @@ export default class VirtVm extends HarvesterResource {
const inStore = this.productInStore;
const allRestore = this.$rootGetters[`${ inStore }/all`](HCI.RESTORE);
const res = allRestore.find((O) => O.id === id);
const res = this.$rootGetters[`${ inStore }/byId`](HCI.RESTORE, id);
if (res) {
const allBackups = this.$rootGetters[`${ inStore }/all`](HCI.BACKUP);
@@ -1073,42 +1099,6 @@ export default class VirtVm extends HarvesterResource {
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() {
return parseVolumeClaimTemplates(this);
}
@@ -1126,7 +1116,6 @@ export default class VirtVm extends HarvesterResource {
get rootImageId() {
let imageId = '';
const inStore = this.productInStore;
const pvcs = this.$rootGetters[`${ inStore }/all`](PVC) || [];
const volumes = this.spec.template.spec.volumes || [];
@@ -1136,9 +1125,7 @@ export default class VirtVm extends HarvesterResource {
});
if (!isNoExistingVolume) {
const existingVolume = pvcs.find(
(P) => P.id === `${ this.metadata.namespace }/${ firstVolumeName }`
);
const existingVolume = this.$rootGetters[`${ inStore }/byId`](PVC, `${ this.metadata.namespace }/${ firstVolumeName }`);
if (existingVolume) {
return existingVolume?.metadata?.annotations?.[
@@ -1287,6 +1274,10 @@ export default class VirtVm extends HarvesterResource {
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() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
}
@@ -1316,8 +1307,7 @@ export default class VirtVm extends HarvesterResource {
}
get isBackupTargetUnavailable() {
const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || [];
const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target');
const backupTargetSetting = this.$rootGetters['harvester/byId'](HCI.SETTING, 'backup-target');
return isBackupTargetSettingUnavailable(backupTargetSetting);
}
@@ -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;
});
}
}
@@ -57,7 +57,11 @@ export default class HciVlanConfig extends HarvesterResource {
get _availableActions() {
const out = super._availableActions;
insertAt(out, 0, this.migrateAction);
const canMigrate = !!this.$getters?.['schemaFor']?.(HCI.VLAN_CONFIG)?.collectionMethods?.find((x) => ['post'].includes(x.toLowerCase()));
if (canMigrate) {
insertAt(out, 0, this.migrateAction);
}
return out;
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "harvester",
"description": "Rancher UI Extension for Harvester",
"version": "1.8.2",
"version": "1.9.0-dev",
"private": false,
"rancher": {
"annotations": {
+22 -1
View File
@@ -123,5 +123,26 @@ export default {
const clusterId = currentCluster.id;
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;
},
};
+32 -12
View File
@@ -31,22 +31,42 @@ export function registerAddonSideNav(store, productName, {
}, 600);
};
const hasAccessibleSchema = (t) => {
try {
return !!store.getters[`${ productName }/schemaFor`]?.(t);
} catch (e) {
return false;
}
};
const showTypes = (visibleTypes) => {
store.commit('type-map/basicType', {
product: productName,
group: navGroup,
types: visibleTypes
});
};
const hideTypes = () => {
const basicTypes = store.state['type-map'].basicTypes[productName];
if (basicTypes) {
types.forEach((t) => delete basicTypes[t]);
}
};
// Adds or removes the resource IDs from the product visibility whitelist.
const setMenuVisibility = (visible) => {
if (visible) {
store.commit('type-map/basicType', {
product: productName,
group: navGroup,
types
});
} else {
// Manually delete the keys from the state object to hide them.
const basicTypes = store.state['type-map'].basicTypes[productName];
const accessibleTypes = visible ? types.filter(hasAccessibleSchema) : [];
if (basicTypes) {
types.forEach((t) => delete basicTypes[t]);
}
// 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();
};
+21
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];
}
-17
View File
@@ -27,23 +27,6 @@ OUTPUT_DIR=dist/${DIR}-embedded
echo "Building..."
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..."
find $OUTPUT_DIR -name "index.html" -mindepth 2 -exec rm {} \;
find $OUTPUT_DIR -type d -empty -depth -exec rmdir {} \;
+896 -2146
View File
File diff suppressed because it is too large Load Diff