Compare commits

..
195 Commits
Author SHA1 Message Date
c12d4b5fba feat: support advance option and creation from DataVolume (#776) (#777)
* 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



* feat: add data migration action on volume page



* refactor: use show advanced options link instead of checkbox



* feat: add feature flag



* feat: add feature flag for dataMigration action



---------




(cherry picked from commit 566e79eda5)

Signed-off-by: Vicente Cheng <freeze.bilsted@gmail.com>
Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: freeze <1615081+Vicente-Cheng@users.noreply.github.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-26 17:33:01 +08:00
mergify[bot]andAndy Lee dc91be11f9 chore: bump version to 1.8.0-rc3 (#774) (#775)
(cherry picked from commit 42ddcfc1fe)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-26 12:35:12 +08:00
mergify[bot]andAndy Lee e5b72499b5 fix: use default value got undefined value in payload (#772) (#773)
(cherry picked from commit ad3decf71f)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-25 17:12:51 +08:00
mergify[bot]andAndy Lee e66037b007 ci: remove the single quota for use commit hash (#767) (#771)
(cherry picked from commit 8083a41df0)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-25 14:13:08 +08:00
mergify[bot]andfreeze c676cad057 chore: pin GH Actions to commit sha (#765) (#766)
(cherry picked from commit 62801b3b13)

Co-authored-by: freeze <1615081+Vicente-Cheng@users.noreply.github.com>
2026-03-25 14:08:49 +08:00
mergify[bot]andAndy Lee 254abd4648 ci: add CODEOWNERS file (#768) (#769)
(cherry picked from commit 46b860260a)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-25 13:42:42 +08:00
renovate[bot] 1df1d3534a deps: update dependency yaml to v2.8.3 (#757)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-22 13:06:18 +00:00
mergify[bot]andAndy Lee ff1e6e6e7f docs: update install instruction doc link (#753) (#754)
(cherry picked from commit 97e93dba0b)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-20 16:53:54 +08:00
mergify[bot]andAndy Lee 622242e4a5 refactor: change rwxNetwork setting to kind json (#751) (#752)
(cherry picked from commit 9a8a709e56)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-20 14:48:49 +08:00
mergify[bot]andAndy Lee d8ea9be174 feat: introduce rwxNetwork setting (#746) (#750)
* feat: add rwxNetwork setting



* fix: network payload



---------


(cherry picked from commit d1949641a7)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-19 16:19:44 +08:00
mergify[bot]andfreeze f8a479cfcf feat: add storage migration operation (#724) (#749)
- add storage migration
    - add cancel storage migration


(cherry picked from commit 9c9f59c939)

Signed-off-by: Vicente Cheng <freeze.bilsted@gmail.com>
Co-authored-by: freeze <1615081+Vicente-Cheng@users.noreply.github.com>
2026-03-19 11:53:24 +08:00
mergify[bot]andAndy Lee 6d5e584113 chore: bump to v1.8.0-rc2 (#747) (#748)
(cherry picked from commit ccc14c7fb9)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-19 11:31:07 +08:00
mergify[bot]andAndy Lee a9eb04195e feat: introduce instance-manager-resources setting (#744) (#745)
(cherry picked from commit 2ba471907e)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-18 17:21:28 +08:00
mergify[bot]andAndy Lee 0441c1f6e3 refactor: remove using resourceName to determine is vGPU device (#741) (#743)
(cherry picked from commit 5aea476f64)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-18 17:16:28 +08:00
mergify[bot]andAndy Lee dde58269d5 fix: typo (#740) (#742)
(cherry picked from commit 519c7d9f1f)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-17 16:33:37 +08:00
mergify[bot]andTim Liou b8111f0ad7 fix: reword the error message to focus on bootable volume (#736) (#738)
(cherry picked from commit a9c392c13f)

Signed-off-by: Tim Liou <tim.liou@suse.com>
Co-authored-by: Tim Liou <tim.liou@suse.com>
2026-03-12 17:46:08 +08:00
mergify[bot]andTim Liou 6d627f82e9 fix: container disks don't need volumeClaimTemplates but volumes (#735) (#737)
(cherry picked from commit 888ec7a50f)

Signed-off-by: Tim Liou <tim.liou@suse.com>
Co-authored-by: Tim Liou <tim.liou@suse.com>
2026-03-12 17:45:40 +08:00
mergify[bot]andAndy Lee cfc7a76fe7 feat: add vGPU filter button and hide the enable/disable passthrough in PCIDevice page (#729) (#732)
* feat: add another filter button



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



* refactor: update with conditionally rendering



---------


(cherry picked from commit 23344e0c07)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-11 15:39:41 +08:00
mergify[bot]andPo Han Huang 71d3067354 feat: ensure the state is pending when perform cloning the efi (#730) (#734)
* feat: ensure the state is pending when perform cloning the efi



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



---------


(cherry picked from commit a2486a7d38)

Signed-off-by: pohanhuang <pohan.huang@suse.com>
Co-authored-by: Po Han Huang <hhcs9527@gmail.com>
2026-03-11 15:39:01 +08:00
mergify[bot]andAndy Lee 9ecc372009 chore: bump to v1.8.0-rc1 (#731) (#733)
(cherry picked from commit df3d249923)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-11 15:34:55 +08:00
Andy Lee 62b80b3cec feat: add Insecure Skip TLS Verify checkbox in cluster-registration-url setting (#716)
* feat: add Insecure Skip TLS Verify checkbox

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

* refactor: set insecureSkipTLSVerify default to false

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

* fix: conflict

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

* refactor: remove unneeded change

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

* fix: get the feature flag in data()

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

* refactor: make data logic simpler

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

* refactor: put tip in info banner

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-09 10:51:20 +08:00
renovate[bot] 1cf94ee550 deps: update dependency @types/node to v20.19.37 (#726)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-08 14:15:49 +08:00
renovate[bot] dfaa9fbe33 deps: update dependency elkjs to ^0.11.0 (#727)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-08 14:15:01 +08:00
Andy Lee 94b1c24479 chore: bump @rancher/shell to v3.0.9-rc.6 (#712)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-08 14:13:46 +08:00
Jack Yu e0b2b9ec57 feat: add support-bundle-file-name setting (#725)
* feat: support customizing support bundle file name

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

* feat: add supportBundleFileNameSetting feature flag

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

---------

Signed-off-by: Jack Yu <jack.yu@suse.com>
2026-03-06 15:51:20 +08:00
Andy Lee 06b38a0f99 fix: import sources page header (#723)
* fix: the import sources page header

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

* refactor: update VMware term

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-05 16:36:15 +08:00
Yiya Chen 4be3634c82 feat: add VPC network topology in detail page (#721)
* feat: add topology

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

* feat: add provider info

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

* refactor: remove comments

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

* fix: exclude default network

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

* feat: add VPC peering

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

* refactor: remove regex

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

* refactor: adjust row height

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

* feat: introduce auto layout

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

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2026-03-04 11:47:00 +08:00
Andy Lee de103ff91e fix: missing Download Logs button in Rancher integration mode (#715)
* fix: missing Download Logs button in Rancher integration mode

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

* refactor: add comment

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-02 16:47:20 +08:00
Andy Lee 37a91601c9 feat: add vmMigrationTimeout in vm-force-reset-policy setting (#718)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-03-02 11:10:52 +08:00
renovate[bot] 9794671ccd deps: update patch digest dependencies (#719)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 11:10:22 +08:00
Volker Theile 337f9186f1 fix(vmimport): Rename list pages according to their URL path (#717)
See https://github.com/harvester/harvester-ui-extension/pull/697#issuecomment-3957918146

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

Signed-off-by: Volker Theile <vtheile@suse.com>
2026-02-26 16:32:40 +08:00
Volker Theile 7c187e894d fix(vmimport): Add missing list page for ovasource (#714)
... and several small improvements.

Signed-off-by: Volker Theile <vtheile@suse.com>
2026-02-25 16:47:15 +08:00
Gaurav Mehta c2df13ad73 fix: changed logic to use power status from underlying inventory (#707)
Signed-off-by: Gaurav Mehta <gaurav.mehta@suse.com>
2026-02-23 20:08:54 +11:00
Tim Liou 93f027a57c fix: shall update hotpluggable when change to a non-iso image (#713)
Signed-off-by: Tim Liou <tim.liou@suse.com>
2026-02-23 17:08:33 +08:00
renovate[bot] da83f04b6c deps: update dependency qs to v6.15.0 (#710)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 11:50:05 +08:00
renovate[bot] a97cb08e3f deps: update patch digest dependencies (#705)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-14 14:22:19 +08:00
Tim Liou 3fdc9f03a3 feat: add cdrom hotplug volume (#703)
Signed-off-by: Tim Liou <tim.liou@suse.com>
2026-02-13 16:33:50 +08:00
Jack Yu c8a613874a feat: add cpu model selection (#702)
Signed-off-by: Jack Yu <jack.yu@suse.com>
2026-02-13 15:40:07 +08:00
Andy Lee 2db7ee7397 feat: show notification if there is VM pending restart (#700)
* feat: show notification if there is VM pending restart

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

* refactor: update based on comments

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

* refactor: calculate count from vm names

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-02-03 16:21:20 +08:00
Andy Lee 8b9b5b41b7 style: set max-height for namespace dropdown menu (#693)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-30 16:43:15 +08:00
Andy Lee 77599900b5 feat: add resume button for upgrade paused node (#698)
* feat: add nodeUpgradeOption setting

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

* feat: add resume button when node paused

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

* feat: add feature flag in v1.7.0

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-30 15:12:00 +08:00
Andy Lee 473c1ba355 fix: this.action typo (#696)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-28 15:49:07 +08:00
Andy Lee 708a95b67b chore: restrict v1.8.0 ui extension to run on rancher 2.14 or above (#695)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-27 22:34:47 +08:00
Andy Lee fecb3de0cf chore: update @rancher/shell to v3.0.9-rc.1 (#694)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-26 15:44:14 +08:00
Jack Yu 0781bde188 feat: add warning message when disabling a device that have not been detached in the backend (#675)
* feat: add wanring message when disabling a device that haven not been detached in the backend

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

* fix: remove unused en-us key

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

---------

Signed-off-by: Jack Yu <jack.yu@suse.com>
2026-01-26 14:07:28 +08:00
Kuan-Po Tseng 0647600e88 fix: use longhorn-static for upgrade vmimage (#690)
Signed-off-by: Cooper Tseng <cooper.tseng@suse.com>
2026-01-22 09:46:20 +08:00
Andy Lee 99dbba7958 fix: remove isCordoned condition (#689)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-21 17:26:53 +08:00
Dominik WombacherandVolker Theile 3dcc50980b feat: Introduce VM Import UI flow pages (#642)
* feat(vmimport): First working side nav attempt

Add vmimport entries when the related resource actually exists aka addon was enabled

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): improved version that uses 'store.watch' instead of polling

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* fix(vmimport): further tuning of dynamic side navi load/unload

Code formatting and commits. Also safeguard if something is wrong with the store

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* refactor(vmimport): separate vmimport side nav entries from dynamic logic

function registerAddonSideNav introduced in utils/dynamic-nav.js

Decouples vmimport side nav entries from the hide/unhide based on addon status logic

Makes it reusable in the UI with other AddOns in the future

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): add custom headers for HCI.VMIMPORT

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): add custom headers for HCI.VMIMPORT_SOURCE_V

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): add custom headers for HCI.VMIMPORT_SOURCE_O

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* fix(vmimport): array instead string passed to configureType

Caused routing issues for CRUD operations

Labels moved to labelTypes section to follow standards

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* fix(vmimport): registerAddonSideNav improved and refactored

Clear comments, code refactoring, additional checks and validations

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* fix(vmimport): show correct status for virtualmachineimport

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): custom list components with ns grouping

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* fix(vmimport): 404 on refresh and missing menu entry

Restores virtualType definitions to register routes synchronously,
preventing 404 errors during page reload.

Updates dynamic-nav to force-fetch addon data if missing, fixing
hidden menu issues on direct page access.

Restores explicit label keys for virtualTypes to ensure correct
naming in the side navigation.

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): add edit form for VirtualMachineImport resource

Adds a UI form for VirtualMachineImport to replace manual YAML editing.
The form fetches VmwareSource and OpenstackSource objects for the
source selection dropdown.

It validates the VM name against RFC-1123 rules and filters out
internal storage classes. Users can also configure network mappings
via a dynamic list.

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): add edit form for VmwareSource

Adds a UI to configure VmwareSource resources including the endpoint
and datacenter fields.

For authentication, users can either select an existing Secret or
enter a username and password directly. The form handles creating
the required Kubernetes Secret in the background when new credentials
are provided.

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): add edit form for OpenstackSource

Custom edit form for OpenstackSource resource. Creates new secret
or lets users select existing secrets. Support all fields the CRD has.

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* chore(vmimport): vmware source default endpoint and datacenter renamed

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): add edit form for OvaSource

OvaSource (new in harvester / vm-import-controller v1.7.0).
Imports VMs from an OVA file using via HTTP or HTTPS.

The form supports URL configuration and optional Basic Auth using a
username and password. Users can also provide an optional CA Certificate
for HTTPS verification and configure advanced HTTP timeout settings.

VirtualMachineImport edit page to updated to include OvaSource in
the source dropdown.

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* chore(vmimport): align tab names on openstacksource

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* fix(vmimport): import { TextArea } from '@components/Form/TextArea' not found

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* fix(vmimport): 'Destination Network' don't list all networks

Online listed the 'mgmt' Network. Adjust to read all Virtual Machine Networks.

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): rename side-nav entry to 'Virtual Machine Imports'

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* fix(vmimport): OvaSource Auth tab throws error selecting existing secret

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* fix(vmimport): Add missing caCert input field to vmware source

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* refactor(vmimport): use 'LabeledInput' instead of 'TextAreaAutoGrow' for cacert fields

Changing the type allows labels to show up in the UI

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* refactor(vmimport): Move vars into types files and reference them

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* refactor(vmimport): Use 'currentProduct' value instead of hardcoded 'harvester' string

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* refactor(vmimport): shorten 'selectedOption.raw' usage

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* refactor(vmimport): Checks to make splice() usage more robust

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* refactor(vmimport): re-use existing rfc1123 val function

Move rfc1123 validation error message to l10n/en-us.yaml

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* fix(vmimport): var name typo in vmi edit rfc1123 check

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): vmi use 'FormValidation' and l10n for labels

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): oss use 'FormValidation' and l10n for labels

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): ovas use 'FormValidation' and l10n for labels

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* feat(vmimport): vms use 'FormValidation' and l10n for labels

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>

* refactor(vmimport): Display error message at the top of the page

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

---------

Signed-off-by: Dominik Wombacher <dominik.wombacher@suse.com>
Signed-off-by: Volker Theile <vtheile@suse.com>
Co-authored-by: Volker Theile <vtheile@suse.com>
2026-01-21 15:30:55 +08:00
renovate[bot] ee1c3de188 deps: update patch digest dependencies (#686)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 11:22:38 +08:00
Kuan-Po Tseng 915559962a fix: use file as field name instead of chunk in cdi vmimage upload (#684)
Signed-off-by: Cooper Tseng <cooper.tseng@suse.com>
2026-01-20 10:57:48 +08:00
Andy Lee b1b1a31c04 fix: do not set cpu.maxSockets on UI (#674)
* fix: do not set cpu.maxSockets for ARM clusters

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

* fix: remove maxSocket to fix bug on ARM cluster

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-16 17:09:13 +08:00
Guilherme Macedo 7f52562d22 ci: add FOSSA scanning workflow (#683)
Signed-off-by: Guilherme Macedo <guilherme@gmacedo.com>
2026-01-16 15:42:33 +08:00
Andy Lee b140c05697 ci: only do package auto update for release-harvester-v1.7 ~ v1.9 (#682)
* ci: disable package auto update for release-harvester-v1.0/v1.5

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

* refactor: only match release v1.7 ~ v1.9 branches

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

* refactor: change basebranches

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-14 15:51:15 +08:00
Andy Lee ad9fef63c0 chore: update copyright year to 2026 (#681)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-13 15:15:50 +08:00
renovate[bot] 786e271ac6 deps: update patch digest dependencies (#676)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 22:28:33 +08:00
freeze c169853e5a chore: bump version to v1.8.0-dev (#672)
Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
2026-01-04 23:19:49 +08:00
Tim Liou 1352246e1e fix: drop mac-address annotation from vm template to prevent MAC address reusing (#663)
related-to: harvester/harvester#9788
related-to: harvester/harvester#9798

Signed-off-by: Tim Liou <tim.liou@suse.com>
2026-01-02 17:24:57 +08:00
renovate[bot] 49374bb18a deps: update dependency qs to v6.14.1 [security] (#664)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-02 16:15:54 +08:00
Andy Lee fe3a12e28c docs: add README.md in pkg/harvester (#661)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-12-23 16:23:01 +08:00
Andy Lee a86302c9d5 chore: bump version to v1.7.0 (#658) 2025-12-22 13:23:54 +08:00
freeze 5fe7e13fcd chore: bump version to v1.7.0-rc7 (#656)
Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
2025-12-16 20:19:22 +08:00
renovate[bot] c079984047 deps: update dependency @types/node to v20.19.27 (#651)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 14:22:49 +08:00
Andy Lee 5769588633 chore: bump version to v1.7.0-rc6 (#649)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-12-11 16:47:48 +08:00
Andy Lee b29950f99c fix: failed to create multiple VMs (#647)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-12-11 16:12:39 +08:00
Andy Lee 6c27a46274 fix: do not inherit template secret when creating new VM (#643)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-12-10 17:12:56 +08:00
Andy Lee b03fffbc30 feat: read addon displayname from label and add descheduler description (#644)
* refactor: display addon name from addon.harvesterhci.io/displayName label

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

* refactor: add descheduler description

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-12-10 15:13:21 +08:00
Andy Lee 5b668a176c feat: integrate cron editor in vm schedule edit page (#635)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-12-10 08:02:58 +08:00
renovate[bot] b4019a2c86 deps: update patch digest dependencies (#630)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 15:08:40 +08:00
freeze 416098ffd8 chore: bump to v1.7.0-rc5 (#636)
Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
2025-12-04 22:40:17 +08:00
Andy Lee 3d7b96d86d chore: bump to v1.7.0-rc4 (#621)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-11-27 17:45:36 +08:00
Caio Torres 0b37467f76 fix: create new secret on vm creation (#614)
* fix: create new secret on vm creation

Signed-off-by: Caio Torres <caio.torres@suse.com>

* fix: ensure parseVM result is immutable

Signed-off-by: Caio Torres <caio.torres@suse.com>

---------

Signed-off-by: Caio Torres <caio.torres@suse.com>
2025-11-27 17:36:55 +08:00
renovate[bot] fab7fbec5e deps: update dependency node-forge to v1.3.2 [security] (#623)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-27 14:32:20 +08:00
Andy Lee d94003f8c2 feat: allow user to attach volume to muliple VMs (#620)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-11-27 14:22:06 +08:00
renovate[bot] dbb5b01cc3 deps: update patch digest dependencies (#615)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 14:56:50 +08:00
Andy Lee 467933bda0 chore: bump version to v1.7.0-rc3 (#612) 2025-11-20 10:19:12 +08:00
Yiya Chen 1b183febdc fix: condition render namespaceOptions (#607)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-11-19 17:35:33 +08:00
Andy Lee 70d3b656f7 fix: change migConfiguration model to inherit from harvester resource (#608)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-11-19 17:30:58 +08:00
Andy Lee 10d19cd329 feat: create related image storageclass before OS upgrade (#595)
* feat: create related image SC before upgrade

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

* refactor: update spec.targetStorageClassName

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>
2025-11-17 17:25:16 +08:00
renovate[bot] 87e44cb658 deps: update dependency @types/node to v20.19.25 (#600)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 17:08:36 +08:00
Yiya Chen 1715ae754c feat: modified placeholder (#599) 2025-11-17 17:05:40 +08:00
30de2b1a18 feat: add support for configuring transparent hugepages (#414)
* feat: add support for configuring transparent hugepages

Related-to: https://github.com/harvester/harvester/issues/5006
Co-authored-by: Moritz Röhrich <moritz.rohrich@suse.com>
Signed-off-by: Tim Serong <tserong@suse.com>

* fix: return empty object if hugepages can't be found for node

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

Co-authored-by: Andy Lee <andy.lee@suse.com>
Signed-off-by: Tim Serong <tserong@suse.com>

---------

Signed-off-by: Tim Serong <tserong@suse.com>
Co-authored-by: Moritz Röhrich <moritz.rohrich@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-12 17:09:56 +11:00
Andy Lee 6fedcc353c chore: bump version to v1.7.0-rc2 (#596)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-11-12 09:14:24 +08:00
Yiya Chen f9bff21e84 feat: support for HotPlugNICs from Kubevirt (#582)
* refactor: rename hotplug volume
* feat: add hotplug NIC
* feat: add hot unplug
* refactor: rename NIC
* feat: get latest status
* feat: disable not ready options
* feat: filter out system networks
* refactor: update wordings

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-11-11 11:43:46 +08:00
Andy Lee 6735826e15 chore: update yarn.lock for @rancher/shell v3.0.8-rc.8 (#591)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-11-10 15:59:11 +08:00
Andy Lee 9e17e239cf chore: bump shell version to v3.0.8-rc.8 (#588)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-11-10 14:53:49 +08:00
Tim Serong a1cf41bda9 feat: enable snapshot and clone for LHv2 (#379)
Now that Longhorn supports volume clone with the V2 data engine, we
can enable volume snapshot and clone.

Related issue: https://github.com/harvester/harvester/issues/6710

Signed-off-by: Tim Serong <tserong@suse.com>
2025-11-06 16:18:37 +11:00
Andy Lee db58024351 ci: lint last commit if is empty string or all zero (#584)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-11-05 15:07:43 +08:00
Andy Lee 81bf19419c chore: bump version to v1.7.0-rc1 (#583)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-11-04 16:07:02 +08:00
Andy Lee 6f90cae482 feat: add vGPU MIG Configuration page (#576)
* feat: add vGPU MIGConfiguration page

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

* feat: add detail page

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

* feat: add banner

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

* refactor: allow editConfig when status is empty

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

* refactor: remove unneeded code

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

* refactor: only show disable action if MIGConfig is enabled

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

* refactor: some UI flow changes

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

* feat: show configured profile in table

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

* refactor: show configured profiles with requested count

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

* refactor: based on review

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-11-04 13:32:30 +08:00
renovate[bot] b4980a51e7 deps: update dependency @types/node to v20.19.24 (#578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 15:32:05 +08:00
Andy Lee 756ed383ac fix: namespace can't be selected the same in IP pool page (#560)
* fix: the namespacess option can't be selected twice in standalone UI

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

* refactor: disabled slected ns

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-31 16:52:05 +08:00
Yiya Chen 7e0a9dcd80 feat: add kubevirt migration setting (#577)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-10-31 16:49:31 +08:00
devsymo 5fae6c3087 fix(image): Correctly handle query parameters in image URL validation (#569)
* fix(image): Correctly handle query parameters in image URL validation

The imageUrl validator currently fails to correctly extract the file extension
when the image URL contains query parameters or fragments (e.g., 'image.qcow2?token=abc').

This change introduces a dedicated function, `getFilenameFromUrl`,
which uses the native URL object for robust parsing.
This ensures the file suffix validation is always performed on the actual filename,
ignoring any trailing parameters.

Signed-off-by: devsymo <devsymo@hotmail.com>

* fix(lint): Resolve formatting and spacing warnings

Signed-off-by: devsymo <devsymo@hotmail.com>

* fix: adjusted filename extraction logic and remove duplicate code

Signed-off-by: DevSymo <DevSymo@hotmail.com>

---------

Signed-off-by: devsymo <devsymo@hotmail.com>
Signed-off-by: DevSymo <DevSymo@hotmail.com>
2025-10-30 16:02:35 +08:00
renovate[bot] a994d9861e deps: update patch digest dependencies (#572)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 11:17:36 +08:00
Andy Lee 5d996c42dc ci: allow any string as commit scope (#571)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-23 14:18:02 +08:00
Andy Lee 88cd302ce4 fix: trunk input field is missing when creating L2VlanTrunkNetwork (#570)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-23 09:47:47 +08:00
Yiya Chen e6dd8d6771 feat: online Volume Resizing from the VM Page (#568)
* feat: add conditions tab
* feat: ignore restart for resizing
* refactor: remove unused code
---------
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-10-22 17:33:28 +08:00
Andy Lee 1a3822881e fix: view and edit mode for l2vlan trunk (#563)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-20 14:52:32 +08:00
renovate[bot] ee8bb21a10 deps: update dependency @types/node to v20.19.22 (#564)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 10:44:54 +08:00
Yiya Chen e3d30a0eec feat: make size editable (#562)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-10-17 15:36:36 +08:00
Andy Lee 532b6c4d50 feat: add access / trunk mode in create VM network page (#510)
* feat: add l2VlanTrunkMode feature

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

* refactor: remove unneeded code

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

* refactor: fix edit l2vlan trunk mode edit page

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

* fix: hide Route tab when trunk mode

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-17 15:28:21 +08:00
Yiya Chen f22644e058 fix: reset filesystem (#559)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-10-17 12:15:58 +08:00
Andy Lee cd128d0444 chore: limit v1.7.0 to install on Rancher 2.13.0+ (#558)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-15 17:38:09 +08:00
Yiya Chen 0278f51260 fix: WebVNC console shortcut keys not working correctly (#557)
* style: disable overflow

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

* style: fix class name

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

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-10-13 16:03:16 +08:00
Andy Lee 98efd63110 fix: abort upload when switch to URL option (#552)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-13 14:48:37 +08:00
renovate[bot] 78e78ac8dd deps: update patch digest dependencies (#553)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-12 22:27:42 +08:00
Yiya Chen 2aaa0a55a2 feat: add default value (#548)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-10-09 13:37:06 +08:00
Andy Lee 5c2a23924d fix: allow to edit as yaml if has empty CPU or memory (#547)
* fix: allow edit as yaml in create VM page if empty CPU or memory

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

* refactor: remove getCPUMemoryValidation

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-08 16:11:27 +08:00
Yiya Chen 7af8a82838 feat: remove machine type dropdown (#546)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-10-08 13:41:35 +08:00
Caio Torres 3b343bcaca fix: create new secret on vm creation (#537)
Signed-off-by: Caio Torres <caio.torres@suse.com>
2025-10-07 17:23:18 +08:00
renovate[bot] 6ddd35d661 deps: update dependency @types/node to v20.19.19 (#542)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 12:01:21 +08:00
Andy Lee bd28ba6f71 feat: add network policy page (#536)
* feat: add Network Policiies page

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

* ci: add build

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

* ci: ensure FROM and TO exist

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

* ci: skip commitlint if FROM and TO emtpy

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

* revert: unnecessary change

* feat: add banner

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-10-03 09:38:31 +08:00
Andy Lee 4bb67153ce fix: set maxSockets : 1 if not enabled cpuMemoryhotplug (#539) 2025-10-02 17:50:16 +08:00
Andy Lee 7dbd442519 fix: force fetch VM data (#535) 2025-09-25 18:55:47 +08:00
Andy Lee 5a0d7f283d fix: unable to create VM when choosing L2VlanNetwork (#534)
* fix: unable to create VM via l2vlan network

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

* refactor: based on discussion

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

* refactor: add comment

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-23 16:32:39 +08:00
Andy Lee 9fdbe9c58f feat: add ACL tab in create subnet page (#527)
* feat: add ACL tab in create subnet page

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

* fix: typo

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-22 16:13:13 +08:00
renovate[bot] f652ed9d4b deps: update dependency @types/node to v20.19.17 (#530)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 10:11:21 +08:00
Andy Lee 21a1cd4e89 feat: skip ksmtuned updated if there no related change (#526)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-22 10:03:34 +08:00
renovate[bot] facc74ca51 deps: update dependency @types/node to v20.19.14 (#522)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 10:14:17 +08:00
Andy Lee 74c12a0d1b feat: add info banner static route tab (#520) 2025-09-13 14:00:18 +08:00
Andy Lee 3277ab4a2b refactor: remove ui-plugin-index setting (#518)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-09 14:55:23 +08:00
Andy Lee 4aabf0b7a3 fix: hide VM take backup action if backup target is not available (#512)
* fix: hide VM take backup action if backup target is not available

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

* refactor: use extracted func

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-09 14:36:23 +08:00
Andy Lee 4e8cb31e9d fix: unable to attach Ti volume in create VM page (#513)
* fix: unable to attach Ti volume in create VM page

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

* refactor: only consider Ti / Gi for attached volume

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-09 14:27:40 +08:00
renovate[bot] 1b214b2b6f deps: update dependency @types/node to v20.19.13 (#514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 14:39:13 +08:00
Andy Lee 748c88866a feat: display N/A in vlanID for overlay network (#511)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-06 11:21:16 +08:00
Andy Lee c3e5c2161e feat: add DHCP ui config in subnet page (#504)
* feat: add dhcp ui setting in subnet page

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

* feat: add dhcp option banner link

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-03 12:04:20 +08:00
Andy Lee f932afee68 refactor: disable the setting detail drawer (#507)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-02 17:47:53 +08:00
renovate[bot] 0bbdf3bf17 deps: update minor dependencies (#493)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 14:44:52 +08:00
renovate[bot] bdf3ac2803 deps: update patch digest dependencies (#492)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 12:14:18 +08:00
Andy Lee 0d183a8174 ci: update renovate.json to remove semanticCommitScope (#506)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-02 12:09:14 +08:00
Andy Lee 8f72c33f4b ci: update renovate.json to let PR title start with deps: (#505) 2025-09-02 11:07:49 +08:00
Caio Torres 22c99211f1 fix: preserve YAML device parameters on config editing (#487)
Signed-off-by: Caio Torres <caio.torres@suse.com>
2025-09-01 14:20:12 +08:00
Andy Lee 775330d829 feat: add reconnect button in VNC console (#489)
* feat: add reconnect button in VNC console

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

* refactor: reconnnect logic

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

* refactor: call reconnnect

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-01 13:51:56 +08:00
freeze 18be022a9f chore: modify the check-version script (#490)
- to pass the dev build

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
2025-08-31 15:11:35 +08:00
Andy Lee 56b4b46b5a chore: add v1.6.1 and v1.7.0 feature flags (#483)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-27 15:50:17 +08:00
Andy Lee db398ecad3 chore: bump version to v1.7.0-dev (#482)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-26 14:54:30 +08:00
Andy Lee d175e3b11a chore: bump version to v1.6.0 (#480)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-26 11:17:58 +08:00
Andy Lee 905db4b12c fix: reboot button not work in VNC console (#475)
* fix: reboot button in VNC console

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

* style: hide the arrow in VNC console

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-25 18:07:26 +08:00
Andy Lee fc31b4bb9d ci: setup env using actions/setup (#478)
* ci: setup env using actions/setup

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

* ci: rerun lint if PR force push

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-25 18:04:32 +08:00
Andy Lee db29a7c31b ci: correct set PR label script name (#476)
* fix: set label script name

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

* ci: rerun PR label after PR edited

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-25 17:40:03 +08:00
Andy Lee 2b3541164d ci: add .github/release.yml (#468)
* ci: add release.yml

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

* refactor: update release.yml

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-22 15:50:11 +08:00
Andy Lee ea5e9aa1f4 ci: auto add PR label workflow and script (#466)
* ci: add auto PR label workflow and script

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

* fix: bug fix prefix

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

* refactor: update label mapping

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

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

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

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

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-22 15:45:54 +08:00
Andy Lee 18a5608e72 ci: only run PR lint in PR stage (#471)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-22 14:42:50 +08:00
Andy Lee 18599fc94c deps: upgrade @rancher/shell to 3.0.5-rc.8 (#469)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-22 10:40:08 +08:00
Andy Lee e155d46483 ci: add PR title lint (#458)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-22 10:38:39 +08:00
Andy Lee 795e5d178f fix: resource page detail title link (#459)
* fix: resource page detail title link

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

* refactor: extract harvester type in a map

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

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

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-15 16:48:38 +08:00
Andy Lee 2e03ac3cf7 fix: spec.domain.memory.guest is lost issue (#462)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-15 11:58:11 +08:00
Andy Lee 76605fdc07 refactor: remove unneeded divider in storage class action menu (#460)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-15 10:51:13 +08:00
Andy Lee c289b001db chore: bump version to v1.6.0-rc6 (#456)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-13 15:05:16 +08:00
Yiya Chen e8d63da1eb fix: fallback default value (#454)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-08-12 13:53:09 +08:00
Andy Lee 4a456ac07d chore: update reviewer (#452)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-05 12:08:06 +08:00
Andy Lee 57dfddd593 chore: bump to 1.6.0-rc5 (#450)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-04 21:31:31 +08:00
Andy Lee 66eac96575 feat: add restore failed message in VM state description (#448)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-04 14:09:26 +08:00
Andy Lee 182d92d80b fix: featureFlags is undefined when installing lower version of ui-ext (#443)
* fix: featureFlags is undefined if install lower version of ui-ext

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-08-01 15:31:20 +08:00
Yiya Chen 1b362f570b fix: update cdiSettings (#444)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-08-01 12:04:14 +08:00
Andy Lee cf798048f8 chore: bump version to v1.6.0-rc4 (#439)
* chore: bump version to v1.6.0-rc4

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

* fix: missing vm network migration setting

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-30 16:05:29 +08:00
Andy Lee 93c8399935 fix: cpu/ memory columns in host detail VM tab (#436)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-29 16:58:22 +08:00
Andy Lee b72f523ddf fix: isStopping state condition (#431)
* fix: isStopping state condition

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

* refactor: remove unused variable

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-29 12:58:38 +08:00
Andy Lee e5ffb08df3 chore: upgrade rancher/shell to 3.0.5-rc.7 (#429)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-28 18:33:37 +08:00
Andy Lee 98a6322c11 refactor: filter anchor link in setting description when searching (#428)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-28 18:19:24 +08:00
Andy Lee be7e4bd80b feat: add runStragety field when cloning a VM (#424)
* feat: add runStragety field when cloning a VM

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

* refactor: remove unneeded change

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-28 14:18:03 +08:00
Yiya ChenandAndy Lee ed2bc3100b feat: expose CDI settings (#418)
* feat: add cdi settings tab

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

* feat: basic layout

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

* feat: add fields mutation

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

* refactor: rename keys

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

* refactor: add edit mode

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

* refactor: remove isCreate

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

* feat: filter volume mode options

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

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-25 13:55:54 +08:00
Yiya Chen b6ffb3e9f1 fix: exception error (#422)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-24 12:37:56 +08:00
Andy Lee 3d8a6bba7e chore: bump version to 1.6.0-rc3 (#420)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-23 15:00:28 +08:00
Andy Lee be9311dc0c feat: CPU / Memory hotplug support (#413)
* feat: add maxCPU and maxMemory

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

* feat: add hotplug dialog

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

* feat: add restart message

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

* feat: let VM template support cpuMemoryHotplug

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

* feat: add feature flag

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

* feat: add max-hotplug-ratio setting

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-23 14:08:59 +08:00
Andy Lee e294f4c00f refactor: remove unneeded translation keys (#416)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-21 17:41:40 +08:00
Nick ChungandAndy Lee a9fa928912 feat: Remove guest cluster in Rancher (#391)
* feat: Remove guest cluster in Rancher

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

* feat: add feature flag 1.6.0

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

* feat: update for review

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

* chore: fix for review

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

* chore: fix for review

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

* refactor: reduce redundant code

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

* chore: change text area to yaml editor

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

* refactor: change radio and yaml editor position

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

---------

Signed-off-by: Nick Chung <nick.chung@suse.com>
Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-21 17:25:51 +08:00
Yiya Chen ef2b4d1589 feat: disable vm volume resize (#409)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-17 18:29:02 +08:00
Andy Lee a73e9f0ac1 ci: disable subject-max-length commitlint rule (#408)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-17 13:51:06 +08:00
Andy Lee 11b3bf4c1f fix: lint in config/types.js (#407)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-16 19:09:25 +08:00
Nick Chung b775ce5f50 chore: disable vmstate-persistence and longhorn-static StorageClasses (#377)
* chore: disable vmstate-persistence and longhorn-static StorageClasses

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

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

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

* chore: remove deletion tooltips in image and volume pages

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

* chore: rollback style changes of image and volume lists

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

---------

Signed-off-by: Nick Chung <nick.chung@suse.com>
2025-07-16 18:21:06 +08:00
Andy Lee 193d4536e3 chore: bump version to v1.6.0-rc2 (#403)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-16 18:09:34 +08:00
Andy Lee 9ca9fdb521 feat: support for subnets and VPCs from UI (#374)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-16 17:51:37 +08:00
Yiya Chen bcabefe9f3 fix: add default value to prevent exception (#397)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-16 15:16:40 +08:00
Andy Lee c541f81dc3 ci: add package version check (#396)
* ci: add package version check

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

* ci: extract check version in a script

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

* ci: update env variable

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-16 13:17:05 +08:00
Yiya Chen 4486f71c8f feat: add vm-migration-network setting (#395)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-16 10:13:07 +08:00
Yiya Chen 060105ead3 chore: upgrade rancher/shell to 3.0.5-rc6 (#393)
* chore: bump shell rc6
* chore: add rancher restriction

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-14 09:56:14 +08:00
Andy Lee 3ea73978ee fix: find the correct vmim based on creationTimeStamp (#389)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-10 11:16:45 +08:00
Yiya Chen f4e363396d feat: add csi-online-expand-validation setting (#378)
* feat: add csi-online-expand-validation setting
* feat: invalid json error message
* feat: handle API errors
* refactor: remove inStore()

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-09 15:26:56 +08:00
Andy Lee fa16e24983 chore: bump version to v1.6.0-rc1 (#385)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-09 14:34:36 +08:00
Andy Lee ce63ee6a19 fix: read volume accessMode even is RWO (#381)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-07-09 11:58:21 +08:00
Yiya Chen ee28697161 ci: ignore draft PR add add backport labels (#382)
* ci: skip draft PR

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

* ci: fix auto assign

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

* ci: add backport label

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

* fix: remove token

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

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-09 11:34:30 +08:00
Yiya Chen 85a06feb91 feat: disable vm images (#376)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-08 15:23:32 +08:00
Yiya Chen 17dd46cee9 ci: add commands_restrictions (#375)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-03 17:46:11 +08:00
Nick Chung 0ef4ff65cc fix: can't migrate network config in Rancher integration (#371)
Signed-off-by: Nick Chung <nick.chung@suse.com>
2025-07-03 16:52:07 +08:00
Yiya Chen cebb302730 ci: use pull_request_target (#370)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-03 14:00:31 +08:00
Andy Lee f12717a8f4 fix: add custom promptRemove for backup and snasphot deletion (#367) 2025-07-03 11:51:53 +08:00
Yiya Chen 03f54643fd ci: remove action-add-labels (#368)
* ci: remove action-add-labels
* ci: use gh cli
* ci: use pull_request_target
* ci: add ref

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-02 14:46:03 +08:00
Yiya Chen 7fb6d44208 ci: add backport label on PR creation (#365)
* ci: improve backport workflow
* chore: ignore tmp files
* ci: add exact match
* ci: target main branch
* ci: rewrite workflows

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-02 11:39:02 +08:00
Nick Chung dc683a50a4 feat: [ENHANCEMENT] Add setting upgrade-config to UI(#8369) (#347)
Signed-off-by: Nick Chung <nick.chung@suse.com>
2025-07-01 11:14:56 +08:00
Yiya Chen be421054d8 feat: SB Enhancements (NS selection and timeout) (#345)
* feat: add namespace field
* feat: add optional inputs
* feat: refine code
* feat: add feature flag
* refactor: fix lint error
* feat: filter default namespaces
* refactor: hide tips with feature flag
* refactor: use UnitInput
* feat: load default value from settings
* refactor: fix API url
* refactor: no available namespaces
* chore: update subject-case rule
---------
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-01 09:38:22 +08:00
Andy Lee fcef0391bb ci: replace tag with github.event.release.tag_name (#356)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-27 17:29:37 +08:00
Andy Lee 1d89aafeab fix: cloned secret should keep originl type (#354)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-27 15:55:47 +08:00
Nick Chung 7386a2deb6 chore: remove run-lint from build-standalone-on-release (#355)
Signed-off-by: Nick Chung <nick.chung@suse.com>
2025-06-27 12:41:22 +08:00
Andy Lee 26f5ebc8e2 ci: auto open backport pr to v1.6 release branch (#353)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-26 17:01:00 +08:00
122 changed files with 10358 additions and 1297 deletions
+1
View File
@@ -14,3 +14,4 @@ dist-pkg
pkg/harvester/index.ts
pkg/harvester/store/*
scripts/pkgfile.js
tmp
+1 -1
View File
@@ -4,7 +4,7 @@ description: Setup node and install dependencies
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
+12 -31
View File
@@ -1,33 +1,14 @@
pull_request_rules:
- name: Automatically open backport PR to release-harvester-v1.5
conditions:
- base=main
- label="require backport 1.5"
actions:
backport:
branches:
- "release-harvester-v1.5"
assignees:
- "{{ author }}"
labels:
- "v1.5 backport PR"
- name: Ask to resolve conflict
conditions:
- conflict
actions:
comment:
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
- name: Automatically open backport PR to release-harvester-v1.0
conditions:
- base=main
- label="require backport 1.0"
actions:
backport:
branches:
- "release-harvester-v1.0"
assignees:
- "{{ author }}"
labels:
- "v1.0 backport PR"
- name: Ask to resolve conflict
conditions:
- conflict
actions:
comment:
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
commands_restrictions:
backport:
conditions:
- or:
- sender-permission>=write
- sender=github-actions[bot]
+3 -2
View File
@@ -1,13 +1,14 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"extends": [
"config:recommended",
":semanticCommitTypeAll(deps)",
":semanticCommitScopeDisabled"
],
"baseBranches": [
"main",
"/^release-harvester-v\\d+\\.\\d+$/"
"release-harvester-v1.7",
"release-harvester-v1.8"
],
"automergeMajor": false,
"semanticCommits": "enabled",
+2 -2
View File
@@ -12,6 +12,6 @@ jobs:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: rancher/gh-issue-mgr/auto-assign-action@main
- uses: rancher/gh-issue-mgr/auto-assign-action@b70f0bdf12a03e5e3f33e4f92ccb6c89deb3ebd9 # main
with:
configuration-path: .github/auto-assign-config.yaml
configuration-path: .github/auto-assign-config.yaml
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.base_ref }}
+49
View File
@@ -0,0 +1,49 @@
name: "[PR Management] Request Backport via Mergify"
on:
pull_request_target:
types: [closed]
branches: [main]
permissions:
pull-requests: write
jobs:
comment-backport:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.base_ref }}
- name: Post Mergify backport command
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
pr_number=${{ github.event.pull_request.number }}
repo="${{ github.repository }}"
labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
labels=$(echo "$labels_json" | jq -r '.[] // empty')
echo "Labels on PR: $labels"
matches=$(echo "$labels" | grep -oE '^require backport/v[0-9]+\.[0-9]+$' || true)
if [[ -z "$matches" ]]; then
echo "No back‑port labels found — skipping."
exit 0
fi
branches=$(echo "$matches" \
| sed -E 's/^require backport\/v/release-harvester-v/' \
| sort -u | tr '\n' ' ')
branches=${branches%% }
cmd="@Mergifyio backport $branches"
echo "Posting Mergify command: $cmd"
gh pr comment "$pr_number" --repo "$repo" --body "$cmd"
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Check package version
env:
@@ -25,12 +25,12 @@ jobs:
name: Build & Upload Hosted
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
with:
fetch-depth: 1
# Note - Cannot use the setup action here as it uses a different yarn install arg
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
@@ -45,19 +45,20 @@ jobs:
run: ./scripts/build-upload-gate
- name: Get gcs auth
uses: rancher-eio/read-vault-secrets@main
uses: rancher-eio/read-vault-secrets@0da85151ad1f19ed7986c41587e45aac1ace74b6 # v3
with:
secrets: |
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
- name: Apply gcs auth
# https://github.com/google-github-actions/auth
uses: 'google-github-actions/auth@v2'
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
with:
credentials_json: "${{ env.GOOGLE_AUTH }}"
- name: Upload build
uses: 'google-github-actions/upload-cloud-storage@v2'
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
# https://github.com/google-github-actions/upload-cloud-storage
with:
path: ${{steps.build-hosted.outputs.BUILD_HOSTED_DIR}}
@@ -71,12 +72,12 @@ jobs:
name: Build & Upload Embedded
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
with:
fetch-depth: 1
# Note - Cannot use the setup action here as it uses a different yarn install arg
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
@@ -89,19 +90,19 @@ jobs:
DISABLED_EMBED_PKG: https://releases.rancher.com/harvester-ui/plugin/harvester-1.0.3.tar.gz
- name: Get gcs auth
uses: rancher-eio/read-vault-secrets@main
uses: rancher-eio/read-vault-secrets@0da85151ad1f19ed7986c41587e45aac1ace74b6 # v3
with:
secrets: |
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
- name: Apply gcs auth
# https://github.com/google-github-actions/auth
uses: 'google-github-actions/auth@v2'
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
with:
credentials_json: "${{ env.GOOGLE_AUTH }}"
- name: Upload tar
uses: 'google-github-actions/upload-cloud-storage@v2'
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
with:
path: ${{steps.build-embedded.outputs.BUILD_EMBEDED_TGZ}}
destination: releases.rancher.com/harvester-ui/dashboard
@@ -114,12 +115,12 @@ jobs:
name: Build & Upload Plugin
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
with:
fetch-depth: 1
# Note - Cannot use the setup action here as it uses a different yarn install arg
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
@@ -133,19 +134,19 @@ jobs:
run: ./scripts/build-upload-gate
- name: Get gcs auth
uses: rancher-eio/read-vault-secrets@main
uses: rancher-eio/read-vault-secrets@0da85151ad1f19ed7986c41587e45aac1ace74b6 # v3
with:
secrets: |
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
- name: Apply gcs auth
# https://github.com/google-github-actions/auth
uses: 'google-github-actions/auth@v2'
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
with:
credentials_json: "${{ env.GOOGLE_AUTH }}"
- name: Upload plugin tar
uses: 'google-github-actions/upload-cloud-storage@v2'
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
with:
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_TARBALL}}
destination: releases.rancher.com/harvester-ui/plugin
@@ -155,7 +156,7 @@ jobs:
process_gcloudignore: false
- name: Upload plugin directory
uses: 'google-github-actions/upload-cloud-storage@v2'
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
with:
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_NAME}}
destination: releases.rancher.com/harvester-ui/plugin/${{steps.ci-build-pkg.outputs.PKG_NAME}}
@@ -27,14 +27,14 @@ jobs:
build-status: ${{ job.status }}
steps:
- name: Read Secrets
uses: rancher-eio/read-vault-secrets@main
uses: rancher-eio/read-vault-secrets@0da85151ad1f19ed7986c41587e45aac1ace74b6 # v3
with:
secrets: |
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | DOCKER_USERNAME ;
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD ;
- name: Checkout repository (normal flow)
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Enable Corepack
run: corepack enable
@@ -45,18 +45,18 @@ jobs:
git config user.email 'github-actions[bot]@users.noreply.github.com'
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
username: ${{ env.DOCKER_USERNAME }}
password: ${{ env.DOCKER_PASSWORD }}
- name: Setup Helm
uses: azure/setup-helm@v3
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
with:
version: v3.8.0
- name: Setup Nodejs with yarn caching
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
cache: yarn
@@ -67,4 +67,4 @@ jobs:
- name: Build and push UI image
run: |
publish="yarn publish-pkgs -cp -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
$publish
$publish
@@ -13,7 +13,7 @@ jobs:
target_branch: ${{ steps.get-version.outputs.target_branch }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Determine target branch
id: get-version
@@ -44,7 +44,7 @@ jobs:
version: ${{ steps.get_version.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Extract version from package.json
id: get_version
@@ -62,7 +62,7 @@ jobs:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup environment
run: |
@@ -70,7 +70,7 @@ jobs:
yarn install --frozen-lockfile
- name: Setup Helm
uses: azure/setup-helm@v3
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
with:
version: v3.8.0
@@ -79,7 +79,7 @@ jobs:
yarn publish-pkgs -s ${{ github.repository }} -b ${{ needs.setup-target-branch.outputs.target_branch }} -t harvester-${{ needs.extract-version.outputs.version }}
- name: Upload charts artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: charts
path: tmp
@@ -94,7 +94,7 @@ jobs:
contents: write
steps:
- name: Checkout release branch
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: '${{ github.ref_name }}'
@@ -105,7 +105,7 @@ jobs:
echo "LAST_COMMIT=${LAST_COMMIT}" >> $GITHUB_ENV
- name: Checkout target branch
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: '${{ needs.setup-target-branch.outputs.target_branch }}'
@@ -121,7 +121,7 @@ jobs:
git config user.email 'github-actions[bot]@users.noreply.github.com'
- name: Download build artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: charts
@@ -132,7 +132,7 @@ jobs:
git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
- name: Run Helm chart releaser
uses: helm/chart-releaser-action@v1.7.0
uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0
with:
charts_dir: ./charts
env:
@@ -17,7 +17,7 @@ jobs:
release_tag: ${{ steps.determine_tag.outputs.release_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Determine release tag
id: determine_tag
@@ -33,7 +33,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Check package version
env:
TAG_VERSION: ${{ github.event.release.tag_name }}
@@ -43,7 +43,7 @@ jobs:
needs:
- setup-release-tag
- check-version
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@9eb70a732e9be146722e1dbab431338366c2afc6 # creators-pkg-v3.0.10
permissions:
actions: write
contents: write
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Check package version
env:
TAG_VERSION: ${{github.ref_name}}
+34
View File
@@ -0,0 +1,34 @@
name: FOSSA Scanning
on:
push:
branches: ["main", "release-harvester-v*"]
workflow_dispatch:
permissions:
contents: read
id-token: write
jobs:
fossa-scanning:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
# The FOSSA token is shared between all repos in Harvester's GH org. It can
# be used directly and there is no need to request specific access to EIO.
- name: Read FOSSA token
uses: rancher-eio/read-vault-secrets@0da85151ad1f19ed7986c41587e45aac1ace74b6 # v3
with:
secrets: |
secret/data/github/org/harvester/fossa/credentials token | FOSSA_API_KEY_PUSH_ONLY
- name: FOSSA scan
uses: fossas/fossa-action@c414b9ad82eaad041e47a7cf62a4f02411f427a0 # v1.8.0
with:
api-key: ${{ env.FOSSA_API_KEY_PUSH_ONLY }}
# Only runs the scan and do not provide/returns any results back to the
# pipeline.
run-tests: false
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.base_ref }}
- name: Setup Nodejs and yarn install
+19 -5
View File
@@ -1,7 +1,7 @@
name: Tests
on:
workflow_call: # This tells GH that the workflow is reusable
workflow_call: # This tells GH that the workflow is reusable
push:
branches:
- main
@@ -16,7 +16,7 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0 # Need full history for commit-lint
@@ -41,8 +41,14 @@ jobs:
FROM="$GITHUB_BASE_SHA"
TO="$GITHUB_HEAD_SHA"
elif [ -n "$GITHUB_BEFORE" ] && [ -n "$GITHUB_AFTER" ]; then
FROM="$GITHUB_BEFORE"
TO="$GITHUB_AFTER"
if [ "$GITHUB_BEFORE" = "0000000000000000000000000000000000000000" ]; then
# first push to HEAD
FROM=""
TO="$GITHUB_AFTER"
else
FROM="$GITHUB_BEFORE"
TO="$GITHUB_AFTER"
fi
else
echo "No valid commit range found, skipping commitlint."
exit 0
@@ -50,7 +56,15 @@ jobs:
echo "FROM=$FROM"
echo "TO=$TO"
npx commitlint --from "$FROM" --to "$TO" --verbose
if [ -z "$FROM" ]; then
echo "Linting last commit $TO"
npx commitlint --last --verbose
else
echo "Linting commits from $FROM to $TO"
npx commitlint --from "$FROM" --to "$TO" --verbose
fi
env:
GITHUB_EVENT_NAME: ${{ github.event_name }}
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
+2
View File
@@ -0,0 +1,2 @@
@a110605
@houhoucoop
+2 -3
View File
@@ -7,7 +7,7 @@ The Harvester UI Extension is a Rancher extension that provides the user interfa
## Installation
For detailed installation instructions, please refer to the [official Harvester documentation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210).
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
## Development Setup
@@ -138,7 +138,6 @@ To validate changes in a release series, switch to the appropriate `vX.Y-head` b
- Examples:
- Test `1.0.x` series → `v1.0-head`
- Test `1.5.x` series → `v1.5-head`
- Test `1.6.x` series → `v1.6-head`
**Steps:**
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
@@ -164,7 +163,7 @@ If you want to contribute, start by reading this document, then visit our [Getti
## License
Copyright (c) 2014-2025 [SUSE, LLC.](https://www.suse.com/)
Copyright (c) 2014-2026 [SUSE, LLC.](https://www.suse.com/)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+4
View File
@@ -22,6 +22,10 @@ module.exports = {
'security', // Security fixes
]
],
// Allows the scope to be empty. [0] means the rule is disabled.
'scope-empty': [0, 'always'],
// Allows any string for the scope.
'scope-case': [2, 'always', 'kebab-case'],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'subject-case': [0, 'never'],
+14 -8
View File
@@ -1,17 +1,22 @@
{
"name": "harvester-ui-extension",
"version": "1.6.1",
"version": "1.8.0-rc3",
"private": false,
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@babel/plugin-transform-class-static-block": "7.28.3",
"@rancher/shell": "3.0.5-rc.8",
"@babel/plugin-transform-class-static-block": "7.28.6",
"@rancher/shell": "3.0.9-rc.6",
"@vue-flow/background": "^1.3.0",
"@vue-flow/controls": "^1.1.1",
"@vue-flow/core": "^1.33.5",
"@vue-flow/minimap": "^1.4.0",
"cache-loader": "^4.1.0",
"color": "4.2.3",
"ip": "2.0.1",
"node-polyfill-webpack-plugin": "^3.0.0",
"elkjs": "^0.11.0",
"vue-draggable-next": "^2.2.1",
"yaml": "^2.5.1"
},
@@ -24,13 +29,13 @@
"glob": "7.2.3",
"glob-parent": "6.0.2",
"json5": "2.2.3",
"@types/lodash": "4.17.20",
"@types/lodash": "4.17.24",
"merge": "2.1.1",
"node-forge": "1.3.1",
"node-forge": "1.3.3",
"nth-check": "2.1.1",
"qs": "6.14.0",
"roarr": "7.21.1",
"semver": "7.7.3",
"qs": "6.15.0",
"roarr": "7.21.4",
"semver": "7.7.4",
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
},
"scripts": {
@@ -38,6 +43,7 @@
"build": "./node_modules/.bin/vue-cli-service build",
"clean": "./node_modules/@rancher/shell/scripts/clean",
"lint": "./node_modules/.bin/eslint --max-warnings 0 --ext .js,.ts,.vue .",
"lint:fix": "./node_modules/.bin/eslint --fix --max-warnings 0 --ext .js,.ts,.vue .",
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
+177
View File
@@ -0,0 +1,177 @@
# harvester-ui-extension
The Harvester UI Extension is a Rancher extension that provides the user interface for [Harvester](https://harvesterhci.io) within the [Rancher Dashboard](https://github.com/rancher/dashboard).
> **Note:**
> This extension is available starting from **Rancher 2.10.0**. Ensure your Rancher version is **2.10.0 or later** to access Harvester integration.
## Installation
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
## Development Setup
Ensure **Node.js v20 or later** is installed for development and debugging.
### Standalone Mode
Run the extension standalone with hot reload at `https://localhost:8005`.
```bash
# Install dependencies
yarn install
# Start the development server
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
# Example with specific server version
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.5.0 API=https://192.168.1.123 yarn dev
```
You may also define environment variables in a `.env` file:
```env
RANCHER_ENV=harvester
VUE_APP_SERVER_VERSION=v1.5.0
API=https://192.168.1.123
```
### Rancher Integration Mode
To run as a Rancher extension, follow the [Rancher UI Extension Guide](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
```bash
API=https://your-rancher-ip yarn dev
```
## Commit Message Guidelines
This project uses [commit-lint](https://commitlint.js.org/) with [Conventional Commits](https://www.conventionalcommits.org/) to ensure consistent and meaningful commit messages.
### Commit Message Format
All commit messages must follow the conventional commit format:
```
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
```
### Supported Types
- **feat**: New features
- **fix**: Bug fixes
- **docs**: Documentation changes
- **style**: Code style changes (formatting, missing semicolons, etc.)
- **refactor**: Code refactoring
- **perf**: Performance improvements
- **test**: Adding or updating tests
- **build**: Build system or external dependencies
- **ci**: CI/CD changes
- **chore**: Other changes that don't modify src or test files
- **revert**: Reverts a previous commit
- **wip**: Work in progress
- **deps**: Dependency updates
- **security**: Security fixes
### Examples
```bash
# Feature
git commit -m "feat: add new virtual machine creation wizard"
# Bug fix
git commit -m "fix: resolve memory leak in VM console"
# Documentation
git commit -m "docs: update installation instructions"
# Breaking change
git commit -m "feat!: change API endpoint structure
BREAKING CHANGE: The /api/v1/vms endpoint has been replaced with /api/v2/vms"
```
### Git Hooks
The project uses [Husky](https://typicode.github.io/husky/) to automatically validate commit messages and run linting before commits:
- **pre-commit**: Runs ESLint to ensure code quality
- **commit-msg**: Validates commit message format using commit-lint
These hooks are automatically installed when you run `yarn install`.
### Manual Validation
You can manually validate commit messages:
```bash
# Validate the last commit
yarn commitlint
# Validate a specific commit
npx commitlint --from <commit-hash>
# Validate a range of commits
npx commitlint --from <start-hash> --to <end-hash>
```
## Branch Structure
- **`main`** – Main development branch
- **`release-harvester-vX.Y`** – Stable release branches per version series
- **`vX.Y-head`** – Testing branches for ongoing changes to extension builds in each release series
> **Note:**
> The `vX.Y-head` branches are auto-generated and kept in sync with release branches. Use these for testing the latest changes in each version series.
## Testing Guidelines
### UI Extension Testing
To validate changes in a release series, switch to the appropriate `vX.Y-head` branch. For main branch testing, use `main-head`.
- Examples:
- Test `1.0.x` series → `v1.0-head`
- Test `1.5.x` series → `v1.5-head`
**Steps:**
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
2. Refresh the Harvester repository using the target `vX.Y-head` branch
3. Go to the **Extensions** page and install the desired version
### Standalone Mode Testing
To test the standalone UI, configure Harvester to load the UI from an external source.
- Examples of `ui-index`:
- Main branch → `https://releases.rancher.com/harvester-ui/dashboard/latest/index.html`
- Release series `1.5.x` → `https://releases.rancher.com/harvester-ui/dashboard/release-harvester-v1.5/index.html`
**Steps:**
1. Go to **Harvester UI** → **Advanced** → **Settings** → **UI**
2. Set **ui-source** to `External`
3. Set **ui-index** to the desired URL
## Contributing
If you want to contribute, start by reading this document, then visit our [Getting Started guide](https://extensions.rancher.io/extensions/next/extensions-getting-started) to learn how to develop and submit changes.
## License
Copyright (c) 2014-2026 [SUSE, LLC.](https://www.suse.com/)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -1,6 +1,8 @@
<script>
import Collapse from '@shell/components/Collapse';
import PercentageBar from '@shell/components/PercentageBar';
import { HCI } from '../types';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
export default {
name: 'HarvesterUpgradeProgressList',
@@ -25,13 +27,45 @@ export default {
}
},
async fetch() {
await this.$store.dispatch('harvester/findAll', { type: HCI.UPGRADE });
},
data() {
return { open: true };
},
computed: {
showResumeButton() {
return this.title === 'Upgrading Node';
},
latestUpgradeCR() {
return this.$store.getters['harvester/all'](HCI.UPGRADE).find( (U) => U.isLatestUpgrade);
},
resumeUpgradePausedNodeEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('resumeUpgradePausedNode');
},
},
methods: {
handleSwitch() {
this.open = !this.open;
},
async resumeNodeUpgrade(nodeName) {
if (!this.latestUpgradeCR || !nodeName) return;
try {
const upgradePauseMapString = this.latestUpgradeCR.metadata.annotations[HCI_ANNOTATIONS.NODE_UPGRADE_PAUSE_MAP] || '{}';
const upgradePauseMap = JSON.parse(upgradePauseMapString);
// update the upgrade CR annotation harvesterhci.io/node-upgrade-pause-map to unpause the node upgrade process
upgradePauseMap[`${ nodeName }`] = 'unpause';
this.latestUpgradeCR.setAnnotation(HCI_ANNOTATIONS.NODE_UPGRADE_PAUSE_MAP, JSON.stringify(upgradePauseMap));
await this.latestUpgradeCR.save();
} catch (e) {
console.error(`unable to update harvester upgrade CR annotations: ${ this.latestUpgradeCR.id }.`, e); // eslint-disable-line no-console
return false;
}
}
}
};
@@ -63,12 +97,28 @@ export default {
v-for="(item, i) in list"
:key="i"
>
<p>
{{ item.name }} <span
class="status"
:class="{ [item.state]: true }"
>{{ item.state }}</span>
</p>
<div class="upgrade-node-header">
<div class="upgrade-node-title">
<p>
{{ item.name }}
</p>
<span
class="status"
:class="{ [item.state]: true }"
>
{{ item.state }}
</span>
</div>
<button
v-if="showResumeButton && resumeUpgradePausedNodeEnabled && item.state === 'Node-upgrade paused'"
type="button"
class="btn bg-info btn-sm"
data-testid="add-item"
@click="resumeNodeUpgrade(item.name)"
>
{{ t('action.resume') }}
</button>
</div>
<PercentageBar
:model-value="item.percent"
preferred-direction="MORE"
@@ -102,10 +152,21 @@ export default {
}
}
.custom-content {
margin-bottom: 14px;
p {
.upgrade-node-title {
flex: 1 0 80%;
margin-right: 10px;
display: flex;
justify-content: space-between;
}
.upgrade-node-header {
display:flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
margin-bottom: 14px;
.status {
float: right;
}
@@ -117,6 +178,8 @@ export default {
}
.warning {
color: var(--error);
margin-bottom: 8px;
margin-top: 4px;
}
}
}
-1
View File
@@ -11,7 +11,6 @@ const CATEGORY = {
ui: [
'branding',
'ui-source',
'ui-plugin-index',
'ui-index',
]
};
@@ -257,6 +257,10 @@ export default {
});
},
reconnect() {
this.$refs.novncConsole.reconnect();
},
softReboot() {
this.vmResource.doSoftReboot();
},
@@ -306,6 +310,13 @@ export default {
{{ t("harvester.action.softreboot") }}
</button>
<button
class="btn btn-sm bg-primary"
@click="reconnect"
>
{{ t("harvester.action.reconnect") }}
</button>
<v-dropdown
v-if="!hideCustomKeysBar"
ref="customKeyPopover"
@@ -380,4 +391,7 @@ export default {
.vm-console .v-popper__popper{
margin-top: 8px;
}
.vm-console .v-popper__inner{
overflow-y: visible;
}
</style>
@@ -0,0 +1,123 @@
<script>
import MessageLink from '@shell/components/MessageLink';
import CreateEditView from '@shell/mixins/create-edit-view';
import { LabeledInput } from '@components/Form/LabeledInput';
import { HCI_SETTING } from '../../config/settings';
import { Checkbox } from '@components/Form/Checkbox';
import { Banner } from '@components/Banner';
export default {
name: 'HarvesterEditClusterRegistrationURL',
components: {
LabeledInput, MessageLink, Checkbox, Banner
},
mixins: [CreateEditView],
data() {
let parseDefaultValue = {};
try {
parseDefaultValue = JSON.parse(this.value.value);
} catch (error) {
parseDefaultValue.url = this.value.value;
parseDefaultValue.insecureSkipTLSVerify = true;
}
return {
parseDefaultValue,
errors: []
};
},
computed: {
toCA() {
return `${ HCI_SETTING.ADDITIONAL_CA }?mode=edit`;
},
clusterRegistrationTLSVerifyEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('clusterRegistrationTLSVerify');
},
registrationURL: {
get() {
return this.clusterRegistrationTLSVerifyEnabled ? this.parseDefaultValue.url : this.parseDefaultValue;
},
set(value) {
if (this.clusterRegistrationTLSVerifyEnabled) {
this.parseDefaultValue.url = value;
} else {
this.parseDefaultValue = value;
}
}
}
},
methods: {
getDefaultValue() {
if (this.clusterRegistrationTLSVerifyEnabled) {
return { url: '', insecureSkipTLSVerify: false };
} else {
return '';
}
},
updateUrl() {
this.update();
},
update() {
if (this.clusterRegistrationTLSVerifyEnabled) {
this.value['value'] = JSON.stringify(this.parseDefaultValue);
} else {
this.value['value'] = this.parseDefaultValue;
}
},
useDefault() {
this.parseDefaultValue = this.getDefaultValue();
},
updateInsecureSkipTLSVerify(newValue) {
const { url = '' } = this.parseDefaultValue;
this.parseDefaultValue = { url, insecureSkipTLSVerify: newValue };
this.update();
},
}
};
</script>
<template>
<div
class="row"
>
<div class="col span-12">
<Banner color="info">
<MessageLink
:to="toCA"
target="_blank"
prefix-label="harvester.setting.clusterRegistrationUrl.tip.prefix"
middle-label="harvester.setting.clusterRegistrationUrl.tip.middle"
suffix-label="harvester.setting.clusterRegistrationUrl.tip.suffix"
/>
</Banner>
<LabeledInput
v-model:value="registrationURL"
class="mb-20"
:mode="mode"
:label="t('harvester.setting.clusterRegistrationUrl.url')"
@update:value="updateUrl"
/>
<div v-if="clusterRegistrationTLSVerifyEnabled">
<Checkbox
v-model:value="parseDefaultValue.insecureSkipTLSVerify"
class="check mb-5"
type="checkbox"
:label="t('harvester.setting.clusterRegistrationUrl.insecureSkipTLSVerify')"
@update:value="updateInsecureSkipTLSVerify"
/>
</div>
</div>
</div>
</template>
@@ -0,0 +1,104 @@
<script>
import UnitInput from '@shell/components/form/UnitInput';
import { Banner } from '@components/Banner';
export default {
name: 'HarvesterInstanceManagerResources',
components: {
UnitInput,
Banner,
},
props: {
value: {
type: Object,
default: () => ({
value: '',
default: '{}'
})
},
},
data() {
const resources = this.parseJSON(this.value?.value) || this.parseJSON(this.value?.default) || {};
return {
resources,
parseError: null,
};
},
methods: {
parseJSON(string) {
try {
return JSON.parse(string);
} catch (e) {
this.parseError = this.t('harvester.setting.instanceManagerResources.parseError', { error: e.message });
return null;
}
},
update() {
if (!this.value) return;
const cpu = { ...this.resources?.cpu };
if (cpu.v1 !== null && cpu.v1 !== undefined) cpu.v1 = String(cpu.v1);
if (cpu.v2 !== null && cpu.v2 !== undefined) cpu.v2 = String(cpu.v2);
this.value.value = JSON.stringify({ ...this.resources, cpu });
},
useDefault() {
if (this.value?.default) {
this.resources = this.parseJSON(this.value.default) || {};
this.update();
}
},
},
};
</script>
<template>
<div>
<Banner
v-if="parseError"
color="error"
>
{{ parseError }}
</Banner>
<div class="row">
<div class="col span-12">
<UnitInput
v-model:value="resources.cpu.v1"
:label="t('harvester.setting.instanceManagerResources.v1')"
suffix="%"
:delay="0"
type="number"
min="0"
max="100"
required
:mode="mode"
class="mb-20"
@update:value="update"
/>
<UnitInput
v-model:value="resources.cpu.v2"
:label="t('harvester.setting.instanceManagerResources.v2')"
suffix="%"
:delay="0"
type="number"
min="0"
max="100"
required
:mode="mode"
class="mb-20"
@update:value="update"
/>
</div>
</div>
</div>
</template>
@@ -0,0 +1,154 @@
<script>
import { LabeledInput } from '@components/Form/LabeledInput';
import { RadioGroup } from '@components/Form/Radio';
import UnitInput from '@shell/components/form/UnitInput';
import { MEBIBYTE } from '../../utils/unit';
import { Banner } from '@components/Banner';
export default {
name: 'KubevirtMigration',
components: {
LabeledInput,
UnitInput,
RadioGroup,
Banner
},
props: {
registerBeforeHook: {
type: Function,
required: true
},
value: {
type: Object,
default: () => ({
value: '',
default: '{}'
})
},
},
data() {
const migration = this.parseJSON(this.value?.value) || this.parseJSON(this.value?.default) || {};
return {
MEBIBYTE,
migration,
parseError: null,
};
},
created() {
this.registerBeforeHook?.(this.willSave, 'willSave');
},
methods: {
parseJSON(string) {
try {
return JSON.parse(string);
} catch (e) {
this.parseError = this.t('kubevirtMigration.parseError', { error: e.message });
// eslint-disable-next-line no-console
console.error('Failed to parse JSON:', e.message);
return null;
}
},
updateValue() {
if (this.value) {
this.value.value = JSON.stringify(this.migration);
}
},
useDefault() {
if (this.value?.default) {
const defaultMigration = this.parseJSON(this.value.default) || {};
this.migration = defaultMigration;
this.updateValue();
}
},
async willSave() {
this.updateValue();
return Promise.resolve();
},
},
};
</script>
<template>
<div>
<Banner
v-if="parseError"
color="error"
>
{{ parseError }}
</Banner>
<div class="migration-field">
<LabeledInput
v-model:value.number="migration.parallelMigrationsPerCluster"
:label="t('harvester.setting.kubevirtMigration.parallelMigrationsPerCluster')"
type="number"
min="1"
/>
<LabeledInput
v-model:value.number="migration.parallelOutboundMigrationsPerNode"
:label="t('harvester.setting.kubevirtMigration.parallelOutboundMigrationsPerNode')"
type="number"
min="1"
/>
<UnitInput
v-model:value="migration.bandwidthPerMigration"
min="0"
:label="t('harvester.setting.kubevirtMigration.bandwidthPerMigration')"
:mode="mode"
:suffix="MEBIBYTE"
:tooltip="t('harvester.setting.kubevirtMigration.bandwidthPerMigrationTooltip', _, true)"
/>
<UnitInput
v-model:value="migration.completionTimeoutPerGiB"
:label="t('harvester.setting.kubevirtMigration.completionTimeoutPerGiB')"
:mode="mode"
:suffix="migration.completionTimeoutPerGiB === 1 ? 'Second' : 'Seconds'"
min="10"
/>
<UnitInput
v-model:value="migration.progressTimeout"
:label="t('harvester.setting.kubevirtMigration.progressTimeout')"
:mode="mode"
:suffix="migration.progressTimeout === 1 ? 'Second' : 'Seconds'"
min="10"
/>
<div
v-for="field in ['allowAutoConverge','allowPostCopy','unsafeMigrationOverride','allowWorkloadDisruption','disableTLS','matchSELinuxLevelOnMigration']"
:key="field"
>
<label
class="mb-5"
:for="field"
>{{ t(`harvester.setting.kubevirtMigration.${field}`) }}</label>
<RadioGroup
:id="field"
v-model:value="migration[field]"
:options="[
{ label: t('advancedSettings.edit.trueOption'), value: true },
{ label: t('advancedSettings.edit.falseOption'), value: false },
]"
/>
</div>
</div>
</div>
</template>
<style scoped>
.migration-field {
display: flex;
flex-direction: column;
gap: 12px;
}
</style>
@@ -0,0 +1,371 @@
<script>
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
import { RadioGroup } from '@components/Form/Radio';
import ArrayList from '@shell/components/form/ArrayList';
import { isValidCIDR } from '@shell/utils/validators/cidr';
import { _EDIT } from '@shell/config/query-params';
import { Banner } from '@components/Banner';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../../types';
import { NETWORK_TYPE } from '../../config/types';
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
const SHARE_STORAGE_NETWORK = 'share-storage-network';
const NETWORK = 'network';
const DEFAULT_DEDICATED_NETWORK = {
vlan: '',
clusterNetwork: '',
range: '',
exclude: [],
};
export default {
name: 'RwxNetworkSetting',
components: {
RadioGroup,
Banner,
ArrayList,
LabeledInput,
LabeledSelect,
},
props: {
registerBeforeHook: {
type: Function,
required: true,
},
mode: {
type: String,
default: _EDIT,
},
value: {
type: Object,
default: () => {
return {};
},
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
});
},
data() {
let enabled = false; // enabled / disabled options
let shareStorageNetwork = false; // shareStorageNetwork / dedicatedRwxNetwork options
let dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
let networkType = L2VLAN;
let exclude = [];
try {
const parsedValue = JSON.parse(this.value.value || this.value.default || '{}');
const parsedNetwork = parsedValue?.[NETWORK] || parsedValue || {};
if (parsedValue && typeof parsedValue === 'object') {
shareStorageNetwork = !!parsedValue[SHARE_STORAGE_NETWORK];
networkType = 'vlan' in parsedNetwork ? L2VLAN : UNTAGGED;
dedicatedNetwork = {
vlan: parsedNetwork.vlan || '',
clusterNetwork: parsedNetwork.clusterNetwork || '',
range: parsedNetwork.range || '',
};
exclude = parsedNetwork?.exclude?.toString().split(',') || [];
enabled = shareStorageNetwork || !!(parsedNetwork.vlan || parsedNetwork.clusterNetwork || parsedNetwork.range);
}
} catch (error) {
enabled = false;
shareStorageNetwork = false;
dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
}
return {
enabled,
shareStorageNetwork,
dedicatedNetwork,
networkType,
exclude,
defaultAddValue: '',
};
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.willSave, 'willSave');
}
},
computed: {
showDedicatedNetworkConfig() {
return this.enabled && !this.shareStorageNetwork;
},
showVlan() {
return this.networkType === L2VLAN;
},
networkTypes() {
return [L2VLAN, UNTAGGED];
},
clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
const clusterNetworksOptions = this.networkType === UNTAGGED ? clusterNetworks.filter((network) => network.id !== 'mgmt') : clusterNetworks;
return clusterNetworksOptions.map((network) => {
const disabled = !network.isReadyForStorageNetwork;
return {
label: disabled ? `${ network.id } (${ this.t('generic.notReady') })` : network.id,
value: network.id,
disabled,
};
});
},
},
methods: {
onUpdateEnabled() {
if (!this.enabled) {
this.shareStorageNetwork = false;
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
}
this.update();
},
onUpdateNetworkType() {
if (this.shareStorageNetwork) {
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
}
this.update();
},
onUpdateDedicatedType(neu) {
this.dedicatedNetwork.clusterNetwork = '';
if (neu === L2VLAN) {
this.dedicatedNetwork.vlan = '';
} else {
delete this.dedicatedNetwork.vlan;
}
this.update();
},
inputVlan(neu) {
if (neu === '') {
this.dedicatedNetwork.vlan = '';
this.update();
return;
}
const newValue = Number(neu);
if (newValue > 4094) {
this.dedicatedNetwork.vlan = 4094;
} else if (newValue < 1) {
this.dedicatedNetwork.vlan = 1;
} else {
this.dedicatedNetwork.vlan = newValue;
}
this.update();
},
useDefault() {
this.enabled = false;
this.shareStorageNetwork = false;
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
this.update();
},
update() {
const value = { [SHARE_STORAGE_NETWORK]: false };
if (this.enabled && this.shareStorageNetwork) {
value[SHARE_STORAGE_NETWORK] = true;
}
if (this.showDedicatedNetworkConfig) {
value[NETWORK] = {};
if (this.showVlan) {
value[NETWORK].vlan = this.dedicatedNetwork.vlan;
}
value[NETWORK].clusterNetwork = this.dedicatedNetwork.clusterNetwork;
value[NETWORK].range = this.dedicatedNetwork.range;
const excludeList = this.exclude.filter((ip) => ip);
if (Array.isArray(excludeList) && excludeList.length > 0) {
value[NETWORK].exclude = excludeList;
}
}
this.value.value = JSON.stringify(value);
},
willSave() {
this.update();
if (!this.showDedicatedNetworkConfig) {
return Promise.resolve();
}
const errors = [];
if (this.showVlan && !this.dedicatedNetwork.vlan) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
}
if (!this.dedicatedNetwork.clusterNetwork) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.clusterNetwork') }, true));
}
if (!this.dedicatedNetwork.range) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.range.label') }, true));
} else if (!isValidCIDR(this.dedicatedNetwork.range)) {
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true));
}
if (this.exclude) {
const hasInvalidCIDR = this.exclude.find((cidr) => {
return cidr && !isValidCIDR(cidr);
});
if (hasInvalidCIDR) {
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', null, true));
}
}
if (errors.length > 0) {
return Promise.reject(errors);
}
return Promise.resolve();
},
},
};
</script>
<template>
<div :class="mode">
<Banner color="warning">
<t
k="harvester.setting.rwxNetwork.warning"
:raw="true"
/>
</Banner>
<RadioGroup
v-model:value="enabled"
class="mb-20"
name="rwx-network-enable"
:options="[true,false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
@update:value="onUpdateEnabled"
/>
<RadioGroup
v-if="enabled"
v-model:value="shareStorageNetwork"
class="mb-20"
name="rwx-network-type"
:options="[true,false]"
:labels="[t('harvester.setting.rwxNetwork.shareStorageNetwork'), t('harvester.setting.rwxNetwork.dedicatedRwxNetwork')]"
@update:value="onUpdateNetworkType"
/>
<Banner
v-if="shareStorageNetwork"
class="mb-20"
color="warning"
>
<t
k="harvester.setting.rwxNetwork.shareStorageNetworkWarning"
:raw="true"
/>
</Banner>
<template v-if="showDedicatedNetworkConfig">
<LabeledSelect
v-model:value="networkType"
class="mb-20"
:options="networkTypes"
:mode="mode"
:label="t('harvester.fields.type')"
required
@update:value="onUpdateDedicatedType"
/>
<LabeledInput
v-if="showVlan"
v-model:value.number="dedicatedNetwork.vlan"
type="number"
class="mb-20"
:mode="mode"
required
placeholder="e.g. 1 - 4094"
label-key="harvester.setting.storageNetwork.vlan"
@update:value="inputVlan"
/>
<LabeledSelect
v-model:value="dedicatedNetwork.clusterNetwork"
label-key="harvester.setting.storageNetwork.clusterNetwork"
class="mb-20"
required
:options="clusterNetworkOptions"
@update:value="update"
/>
<LabeledInput
v-model:value="dedicatedNetwork.range"
class="mb-5"
:mode="mode"
required
:placeholder="t('harvester.setting.storageNetwork.range.placeholder')"
label-key="harvester.setting.storageNetwork.range.label"
@update:value="update"
/>
<ArrayList
v-model:value="exclude"
:show-header="true"
:default-add-value="defaultAddValue"
:mode="mode"
:add-label="t('harvester.setting.storageNetwork.exclude.addIp')"
class="mt-20"
@update:value="update"
>
<template #column-headers>
<div class="box mb-10">
<div class="key">
{{ t('harvester.setting.storageNetwork.exclude.label') }}
</div>
</div>
</template>
<template #columns="scope">
<div class="key">
<input
v-model="scope.row.value"
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
@update:value="update"
/>
</div>
</template>
</ArrayList>
</template>
</div>
</template>
@@ -18,16 +18,20 @@ export default {
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
try {
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
const response = await this.$store.dispatch('harvester/request', { url });
if (this.customSupportBundleFeatureEnabled) {
try {
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
const response = await this.$store.dispatch('harvester/request', { url });
this.defaultNamespaces = response.data || [];
} catch (error) {
this.defaultNamespaces = response.data || [];
} catch (error) {
this.defaultNamespaces = [];
}
} else {
this.defaultNamespaces = [];
} finally {
this.loading = false;
}
this.loading = false;
},
data() {
@@ -42,24 +46,38 @@ export default {
},
computed: {
customSupportBundleFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
},
allNamespaces() {
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
},
filteredNamespaces() {
if (!this.customSupportBundleFeatureEnabled) {
return this.allNamespaces;
}
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
},
namespaceOptions() {
const mappedNamespaces = this.filteredNamespaces.map((ns) => ({ label: ns, value: ns }));
if (!this.customSupportBundleFeatureEnabled) {
return mappedNamespaces;
}
const allSelected =
this.namespaces.length === this.filteredNamespaces.length &&
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
return [controlOption, ...this.filteredNamespaces];
return [controlOption, ...mappedNamespaces];
}
},
@@ -4,6 +4,8 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { RadioGroup } from '@components/Form/Radio';
import { mapGetters } from 'vuex';
import { allHash } from '@shell/utils/promise';
import { NODE } from '@shell/config/types';
export default {
name: 'HarvesterUpgradeConfig',
@@ -15,6 +17,13 @@ export default {
},
mixins: [CreateEditView],
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = { nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }) };
await allHash(hash);
},
data() {
let parseDefaultValue = {};
@@ -39,7 +48,25 @@ export default {
{ value: 'skip', label: 'skip' },
{ value: 'parallel', label: 'parallel' }
];
}
},
nodeUpgradeOptions() {
return [
{ value: 'auto', label: 'auto' },
{ value: 'manual', label: 'manual' }
];
},
nodesOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const nodes = this.$store.getters[`${ inStore }/all`](NODE);
return nodes.map((node) => ({ value: node.id, label: node.name }));
},
showPauseNodes() {
return this.parseDefaultValue.nodeUpgradeOption?.strategy?.mode === 'manual';
},
resumeUpgradePausedNodeEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('resumeUpgradePausedNode');
},
},
created() {
@@ -48,6 +75,18 @@ export default {
methods: {
normalizeValue(obj) {
// handle nodeUpgradeOption.strategy
if (obj?.nodeUpgradeOption?.strategy?.mode === 'auto') {
delete obj.nodeUpgradeOption.strategy.pauseNodes;
}
if (obj?.nodeUpgradeOption?.strategy?.mode === 'manual') {
if (!Array.isArray(obj.nodeUpgradeOption.strategy.pauseNodes)) {
obj.nodeUpgradeOption.strategy.pauseNodes = this.nodesOptions.map((node) => node.value);
}
}
// handle imagePreloadOption.strategy
if (!obj.imagePreloadOption) {
obj.imagePreloadOption = { strategy: { type: 'sequential' } };
}
@@ -105,8 +144,8 @@ export default {
this.update();
},
deep: true
}
}
},
},
};
</script>
@@ -144,6 +183,28 @@ export default {
:labels="[t('generic.enabled'), t('generic.disabled')]"
@update:value="update"
/>
<div v-if="resumeUpgradePausedNodeEnabled">
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.nodeUpgradeOption') }}</b></label>
<LabeledSelect
v-model:value="parseDefaultValue.nodeUpgradeOption.strategy.mode"
class="mb-20 label-select"
:mode="mode"
:label="t('harvester.setting.upgrade.strategy')"
:options="nodeUpgradeOptions"
@update:value="update"
/>
<LabeledSelect
v-if="showPauseNodes"
v-model:value="parseDefaultValue.nodeUpgradeOption.strategy.pauseNodes"
class="mb-20 label-select"
:clearable="true"
:multiple="true"
:mode="mode"
:label="t('harvester.setting.upgrade.pauseNodes')"
:options="nodesOptions"
@update:value="update"
/>
</div>
<div
v-if="errors.length"
class="error"
@@ -69,6 +69,15 @@ export default {
:mode="mode"
label-key="harvester.setting.vmForceDeletionPolicy.period"
/>
<LabeledInput
v-if="parseDefaultValue.enable"
v-model:value.number="parseDefaultValue.vmMigrationTimeout"
class="mb-20"
type="number"
:mode="mode"
label-key="harvester.setting.vmForceDeletionPolicy.vmMigrationTimeout"
/>
</div>
</div>
</template>
+10 -7
View File
@@ -1,10 +1,13 @@
// suffix of doc link, see utils/feature-flags.js how to get complete doc link
export const DOC = {
CONSOLE_URL: `/host/#remote-console`,
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
UPGRADE_URL: `/upgrade/index`,
UPGRADE_CONFIG_URL: `/advanced/index#upgrade-config`,
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
CONSOLE_URL: `/host/#remote-console`,
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
UPGRADE_URL: `/upgrade/index`,
UPGRADE_CONFIG_URL: `/advanced/index#upgrade-config`,
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
VPC_CONFIGURATION_EXAMPLES: `/networking/kubeovn-vpc#vpc-peering-configuration-examples`,
NETWORK_POLICY: `/networking/kubeovn-vm-isolation/#network-policies`,
TRANSPARENT_HUGEPAGES: `https://docs.kernel.org/admin-guide/mm/transhuge.html`,
};
+18 -1
View File
@@ -39,7 +39,6 @@ const FEATURE_FLAGS = {
],
'v1.5.1': [],
'v1.6.0': [
'vmMachineTypes',
'customSupportBundle',
'csiOnlineExpandValidation',
'vmNetworkMigration',
@@ -50,6 +49,24 @@ const FEATURE_FLAGS = {
'vmCloneRunStrategy',
],
'v1.6.1': [],
'v1.7.0': [
'vmMachineTypeAuto',
'lhV2VolExpansion',
'l2VlanTrunkMode',
'kubevirtMigration',
'hotplugNic',
'resumeUpgradePausedNode',
],
'v1.7.1': [],
'v1.8.0': [
'hotplugCdRom',
'supportBundleFileNameSetting',
'clusterRegistrationTLSVerify',
'vGPUAsPCIDevice',
'instanceManagerResourcesSetting',
'rwxNetworkSetting',
'createPVCWithDataVolume'
],
};
const generateFeatureFlags = () => {
+188 -1
View File
@@ -10,6 +10,7 @@ import {
LOGGING,
STORAGE_CLASS,
SECRET,
NETWORK_POLICY
} from '@shell/config/types';
import { HCI, VOLUME_SNAPSHOT } from '../types';
import {
@@ -34,8 +35,21 @@ import {
SNAPSHOT_TARGET_VOLUME,
IMAGE_VIRTUAL_SIZE,
IMAGE_STORAGE_CLASS,
HARVESTER_DESCRIPTION
HARVESTER_DESCRIPTION,
VM_IMPORT_SOURCE_VM,
VM_IMPORT_SOURCE_CLUSTER,
VM_IMPORT_STATUS,
VM_IMPORT_SOURCE_V_DC,
VM_IMPORT_SOURCE_V_ENDPOINT,
VM_IMPORT_SOURCE_V_STATUS,
VM_IMPORT_SOURCE_O_REGION,
VM_IMPORT_SOURCE_O_ENDPOINT,
VM_IMPORT_SOURCE_O_STATUS,
VM_IMPORT_SOURCE_OVA_URL,
VM_IMPORT_SOURCE_OVA_STATUS,
} from './table-headers';
import { ADD_ONS } from './harvester-map';
import { registerAddonSideNav } from '../utils/dynamic-nav';
const TEMPLATE = HCI.VM_VERSION;
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
@@ -194,6 +208,142 @@ export function init($plugin, store) {
exact: false
});
// ===========================================================================
// VM Import Controller UI Flow
// ===========================================================================
// Define group (Hidden by default)
weightGroup('vmimport', 0, false);
// VirtualMachineImport
headers(HCI.VMIMPORT, [
STATE,
NAME_COL,
NAMESPACE_COL,
VM_IMPORT_SOURCE_VM,
VM_IMPORT_SOURCE_CLUSTER,
VM_IMPORT_STATUS,
AGE
]);
configureType(HCI.VMIMPORT, {
resource: HCI.VMIMPORT,
resourceDetail: HCI.VMIMPORT,
resourceEdit: HCI.VMIMPORT,
location: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT }
}
});
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
name: HCI.VMIMPORT,
labelKey: 'harvester.addons.vmImport.labels.vmimport',
group: 'vmimport',
namespaced: true,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT }
}
});
// Source: VMware
headers(HCI.VMIMPORT_SOURCE_V, [
STATE,
NAME_COL,
VM_IMPORT_SOURCE_V_ENDPOINT,
VM_IMPORT_SOURCE_V_DC,
VM_IMPORT_SOURCE_V_STATUS,
AGE
]);
configureType(HCI.VMIMPORT_SOURCE_V, {
resource: HCI.VMIMPORT_SOURCE_V,
resourceDetail: HCI.VMIMPORT_SOURCE_V,
resourceEdit: HCI.VMIMPORT_SOURCE_V,
location: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_V }
}
});
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
name: HCI.VMIMPORT_SOURCE_V,
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare',
group: 'vmimport',
namespaced: true,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_V }
}
});
// Source: OpenStack
headers(HCI.VMIMPORT_SOURCE_O, [
STATE,
NAME_COL,
VM_IMPORT_SOURCE_O_ENDPOINT,
VM_IMPORT_SOURCE_O_REGION,
VM_IMPORT_SOURCE_O_STATUS,
AGE
]);
configureType(HCI.VMIMPORT_SOURCE_O, {
resource: HCI.VMIMPORT_SOURCE_O,
resourceDetail: HCI.VMIMPORT_SOURCE_O,
resourceEdit: HCI.VMIMPORT_SOURCE_O,
location: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_O }
}
});
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
name: HCI.VMIMPORT_SOURCE_O,
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack',
group: 'vmimport',
namespaced: true,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_O }
}
});
// Source: OVA
headers(HCI.VMIMPORT_SOURCE_OVA, [
STATE,
NAME_COL,
VM_IMPORT_SOURCE_OVA_URL,
VM_IMPORT_SOURCE_OVA_STATUS,
AGE
]);
configureType(HCI.VMIMPORT_SOURCE_OVA, {
resource: HCI.VMIMPORT_SOURCE_OVA,
resourceDetail: HCI.VMIMPORT_SOURCE_OVA,
resourceEdit: HCI.VMIMPORT_SOURCE_OVA,
location: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_OVA }
}
});
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
name: HCI.VMIMPORT_SOURCE_OVA,
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA',
group: 'vmimport',
namespaced: true,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_OVA }
}
});
// Enable SideNav based on Addon Status
registerAddonSideNav(store, PRODUCT_NAME, {
addonName: ADD_ONS.VM_IMPORT_CONTROLLER,
resourceType: HCI.ADD_ONS,
navGroup: 'vmimport',
types: [
HCI.VMIMPORT_SOURCE_V,
HCI.VMIMPORT_SOURCE_O,
HCI.VMIMPORT_SOURCE_OVA,
HCI.VMIMPORT
]
});
// ===========================================================================
basicType([HCI.VOLUME]);
configureType(HCI.VOLUME, {
location: {
@@ -425,6 +575,7 @@ export function init($plugin, store) {
HCI.CLUSTER_NETWORK,
HCI.NETWORK_ATTACHMENT,
HCI.VPC,
NETWORK_POLICY,
HCI.LB,
HCI.IP_POOL,
],
@@ -455,6 +606,7 @@ export function init($plugin, store) {
HCI.PCI_DEVICE,
HCI.SR_IOVGPU_DEVICE,
HCI.VGPU_DEVICE,
HCI.MIG_CONFIGURATION,
HCI.USB_DEVICE,
HCI.ADD_ONS,
HCI.SECRET,
@@ -566,6 +718,21 @@ export function init($plugin, store) {
ifHaveType: HCI.VPC,
});
configureType(NETWORK_POLICY, { hiddenNamespaceGroupButton: true, canYaml: false });
virtualType({
labelKey: 'harvester.networkPolicy.label',
name: NETWORK_POLICY,
namespaced: true,
weight: 186,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: NETWORK_POLICY }
},
exact: false,
ifHaveType: NETWORK_POLICY,
});
configureType(HCI.SNAPSHOT, {
isCreatable: false,
location: {
@@ -832,6 +999,26 @@ export function init($plugin, store) {
]
});
virtualType({
labelKey: 'harvester.migconfiguration.label',
group: 'advanced',
weight: 12,
name: HCI.MIG_CONFIGURATION,
namespaced: false,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.MIG_CONFIGURATION }
},
exact: false,
ifHaveType: HCI.MIG_CONFIGURATION,
});
configureType(HCI.MIG_CONFIGURATION, {
isCreatable: false,
hiddenNamespaceGroupButton: true,
canYaml: false,
});
virtualType({
labelKey: 'harvester.usb.label',
group: 'advanced',
+6
View File
@@ -39,6 +39,12 @@ export const VOLUME_TYPE = [{
value: 'cd-rom'
}];
export const VOLUME_HOTPLUG_ACTION = {
INSERT_CDROM_IMAGE: 'INSERT_CDROM_IMAGE',
EJECT_CDROM_IMAGE: 'EJECT_CDROM_IMAGE',
DETACH_DISK: 'DETACH_DISK'
};
export const ACCESS_CREDENTIALS = {
RESET_PWD: 'userPassword',
INJECT_SSH: 'sshPublicKey'
@@ -28,6 +28,7 @@ export const HCI = {
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
CLONE_BACKEND_STORAGE_STATUS: 'harvesterhci.io/clone-backend-storage-status',
MIGRATION_STATE: 'harvesterhci.io/migrationState',
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
IMAGE_NAME: 'harvesterhci.io/image-name',
@@ -50,6 +51,7 @@ export const HCI = {
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
ADDON_DISPLAYNAME: 'addon.harvesterhci.io/displayName',
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
@@ -76,4 +78,7 @@ export const HCI = {
CLONE_STRATEGY: 'cdi.harvesterhci.io/storageProfileCloneStrategy',
VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes',
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
MAC_ADDRESS: 'harvesterhci.io/mac-address',
NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map',
CDI_POPULATOR_KIND: 'cdi.kubevirt.io/storage.populator.kind',
};
+19 -4
View File
@@ -4,7 +4,6 @@ export const HCI_SETTING = {
LOG_LEVEL: 'log-level',
SERVER_VERSION: 'server-version',
UI_INDEX: 'ui-index',
UI_PLUGIN_INDEX: 'ui-plugin-index',
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
UPGRADE_CHECKER_URL: 'upgrade-checker-url',
VLAN: 'vlan',
@@ -17,9 +16,11 @@ export const HCI_SETTING = {
DEFAULT_STORAGE_CLASS: 'default-storage-class',
SUPPORT_BUNDLE_TIMEOUT: 'support-bundle-timeout',
SUPPORT_BUNDLE_EXPIRATION: 'support-bundle-expiration',
SUPPORT_BUNDLE_FILE_NAME: 'support-bundle-file-name',
SUPPORT_BUNDLE_IMAGE: 'support-bundle-image',
SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT: 'support-bundle-node-collection-timeout',
STORAGE_NETWORK: 'storage-network',
RWX_NETWORK: 'rwx-network',
VM_FORCE_RESET_POLICY: 'vm-force-reset-policy',
SSL_CERTIFICATES: 'ssl-certificates',
SSL_PARAMETERS: 'ssl-parameters',
@@ -38,7 +39,9 @@ export const HCI_SETTING = {
UPGRADE_CONFIG: 'upgrade-config',
VM_MIGRATION_NETWORK: 'vm-migration-network',
RANCHER_CLUSTER: 'rancher-cluster',
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio'
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
KUBEVIRT_MIGRATION: 'kubevirt-migration',
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources'
};
export const HCI_ALLOWED_SETTINGS = {
@@ -71,11 +74,17 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: { kind: 'number' },
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: { kind: 'number' },
[HCI_SETTING.SUPPORT_BUNDLE_FILE_NAME]: {
kind: 'string', canReset: true, featureFlag: 'supportBundleFileNameSetting'
},
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { kind: 'number', featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' },
[HCI_SETTING.STORAGE_NETWORK]: {
kind: 'custom', from: 'import', canReset: true
},
[HCI_SETTING.RWX_NETWORK]: {
kind: 'json', from: 'import', canReset: true, featureFlag: 'rwxNetworkSetting'
},
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
[HCI_SETTING.SSL_PARAMETERS]: {
@@ -84,7 +93,6 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES]: { from: 'import', canReset: true },
[HCI_SETTING.AUTO_DISK_PROVISION_PATHS]: { canReset: true },
[HCI_SETTING.RELEASE_DOWNLOAD_URL]: { kind: 'url' },
[HCI_SETTING.UI_PLUGIN_INDEX]: { kind: 'url' },
[HCI_SETTING.CONTAINERD_REGISTRY]: {
kind: 'json', from: 'import', canReset: true
},
@@ -117,11 +125,18 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.VM_MIGRATION_NETWORK]: {
kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration',
},
[HCI_SETTING.KUBEVIRT_MIGRATION]: {
kind: 'json', from: 'import', canReset: true, featureFlag: 'kubevirtMigration',
},
[HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: {
kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting'
}
};
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
[HCI_SETTING.CLUSTER_REGISTRATION_URL]: {
kind: 'url',
kind: 'custom',
from: 'import',
canReset: true,
},
[HCI_SETTING.UI_PL]: {
+99
View File
@@ -131,3 +131,102 @@ export const PROVIDER = {
value: 'spec.provider',
align: 'left',
};
// Source VM column in migration.harvesterhci.io.virtualmachineimport list page
export const VM_IMPORT_SOURCE_VM = {
name: 'sourceVm',
labelKey: 'harvester.tableHeaders.vmImportSourceVm',
value: 'spec.virtualMachineName',
sort: 'spec.virtualMachineName',
align: 'left',
};
// Source Cluster column in migration.harvesterhci.io.virtualmachineimport list page
export const VM_IMPORT_SOURCE_CLUSTER = {
name: 'sourceCluster',
labelKey: 'harvester.tableHeaders.vmImportSourceCluster',
value: 'spec.sourceCluster.name',
sort: 'spec.sourceCluster.name',
align: 'left',
};
// Import Status column in migration.harvesterhci.io.virtualmachineimport list page
export const VM_IMPORT_STATUS = {
name: 'importStatus',
labelKey: 'harvester.tableHeaders.vmImportStatus',
value: 'status.importStatus',
sort: 'status.importStatus',
align: 'left',
};
// Datacenter column in migration.harvesterhci.io.vmwaresource list page
export const VM_IMPORT_SOURCE_V_DC = {
name: 'datacenter',
labelKey: 'harvester.tableHeaders.vmImportSourceVDatacenter',
value: 'spec.dc',
sort: 'spec.dc',
align: 'left',
};
// Endpoint column in migration.harvesterhci.io.vmwaresource list page
export const VM_IMPORT_SOURCE_V_ENDPOINT = {
name: 'endpoint',
labelKey: 'harvester.tableHeaders.vmImportSourceVEndpoint',
value: 'spec.endpoint',
sort: 'spec.endpoint',
align: 'left',
};
// Cluster Status column in migration.harvesterhci.io.vmwaresource list page
export const VM_IMPORT_SOURCE_V_STATUS = {
name: 'clusterStatus',
labelKey: 'harvester.tableHeaders.vmImportSourceVClusterStatus',
value: 'status.status',
sort: 'status.status',
align: 'left',
};
// Region column in migration.harvesterhci.io.openstacksource list page
export const VM_IMPORT_SOURCE_O_REGION = {
name: 'region',
labelKey: 'harvester.tableHeaders.vmImportSourceORegion',
value: 'spec.region',
sort: 'spec.region',
align: 'left',
};
// Endpoint column in migration.harvesterhci.io.openstacksource list page
export const VM_IMPORT_SOURCE_O_ENDPOINT = {
name: 'endpoint',
labelKey: 'harvester.tableHeaders.vmImportSourceOEndpoint',
value: 'spec.endpoint',
sort: 'spec.endpoint',
align: 'left',
};
// Cluster Status column in migration.harvesterhci.io.openstacksource list page
export const VM_IMPORT_SOURCE_O_STATUS = {
name: 'clusterStatus',
labelKey: 'harvester.tableHeaders.vmImportSourceOClusterStatus',
value: 'status.status',
sort: 'status.status',
align: 'left',
};
// URL column in migration.harvesterhci.io.ovasource list page
export const VM_IMPORT_SOURCE_OVA_URL = {
name: 'url',
labelKey: 'harvester.tableHeaders.vmImportSourceOVAUrl',
value: 'spec.url',
sort: 'spec.url',
align: 'left',
};
// Status column in migration.harvesterhci.io.ovasource list page
export const VM_IMPORT_SOURCE_OVA_STATUS = {
name: 'status',
labelKey: 'harvester.tableHeaders.vmImportSourceOVAStatus',
value: 'status.status',
sort: 'status.status',
align: 'left',
};
+26 -3
View File
@@ -4,9 +4,10 @@ export const BACKUP_TYPE = {
};
export const NETWORK_TYPE = {
L2VLAN: 'L2VlanNetwork',
UNTAGGED: 'UntaggedNetwork',
OVERLAY: 'OverlayNetwork',
L2VLAN: 'L2VlanNetwork',
UNTAGGED: 'UntaggedNetwork',
OVERLAY: 'OverlayNetwork',
L2TRUNK_VLAN: 'L2VlanTrunkNetwork',
};
export const VOLUME_MODE = {
@@ -23,3 +24,25 @@ export const INTERNAL_STORAGE_CLASS = {
VMSTATE_PERSISTENCE: 'vmstate-persistence',
LONGHORN_STATIC: 'longhorn-static',
};
export const L2VLAN_MODE = {
ACCESS: 'access',
TRUNK: 'trunk',
};
export const VMIMPORT_SOURCE_PROVIDER = {
VMWARE: 'vmware',
OPENSTACK: 'openstack',
OVA: 'ova',
};
export const VMIMPORT_SOURCE_KINDS = {
VMWARE: 'VmwareSource',
OPENSTACK: 'OpenstackSource',
OVA: 'OvaSource',
};
export const CDI_POPULATOR_KIND = {
VOLUME_IMPORT_SOURCE: 'VolumeImportSource',
VOLUME_CLONE_SOURCE: 'VolumeCloneSource',
};
@@ -0,0 +1,124 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import ResourceTabs from '@shell/components/form/ResourceTabs';
import Tab from '@shell/components/Tabbed/Tab';
import SortableTable from '@shell/components/SortableTable';
export default {
components: {
ResourceTabs,
Tab,
SortableTable,
},
mixins: [CreateEditView],
props: {
value: {
type: Object,
default: () => {
return {};
}
}
},
computed: {
headers() {
return [
{
name: 'profileName',
labelKey: 'harvester.migconfiguration.tableHeaders.profileName',
value: 'name',
width: 75,
sort: 'name',
dashIfEmpty: true,
},
{
name: 'vGPUID',
labelKey: 'harvester.migconfiguration.tableHeaders.vGPUID',
value: 'vGPUID',
width: 75,
sort: 'vGPUID',
dashIfEmpty: true,
},
{
name: 'available',
labelKey: 'harvester.migconfiguration.tableHeaders.available',
value: 'available',
width: 75,
sort: 'available',
align: 'center',
dashIfEmpty: true,
},
{
name: 'requested',
labelKey: 'harvester.migconfiguration.tableHeaders.requested',
value: 'requested',
width: 75,
sort: 'requested',
align: 'center',
dashIfEmpty: true,
},
{
name: 'total',
labelKey: 'harvester.migconfiguration.tableHeaders.total',
value: 'total',
width: 75,
sort: 'total',
align: 'center',
dashIfEmpty: true,
},
];
},
rows() {
let out = (this.value?.status?.profileStatus || []).map((profile) => {
const {
id, name, total, available
} = profile;
return {
id,
name,
total,
available,
vGPUID: profile.vGPUID?.join(', ') || '',
};
});
out = out.map((row) => {
const requested = this.value?.spec?.profileSpec.find((p) => p.id === row.id)?.requested || 0;
return { ...row, requested };
});
return out;
},
},
};
</script>
<template>
<ResourceTabs
:value="value"
:need-events="false"
:need-related="false"
:mode="mode"
>
<Tab
name="Profile Status"
:label="t('harvester.migconfiguration.profileStatus')"
>
<SortableTable
:headers="headers"
:rows="rows"
key-field="condition"
default-sort-by="condition"
:table-actions="false"
:row-actions="false"
:search="false"
/>
</Tab>
</ResourceTabs>
</template>
@@ -0,0 +1,119 @@
<script>
import LabelValue from '@shell/components/LabelValue';
import { HCI } from '../../types';
import { DOC } from '../../config/doc-links';
export default {
name: 'HarvesterHugepages',
components: { LabelValue },
props: {
node: {
type: Object,
required: true,
},
},
computed: {
docsTransparentHugepagesLink() {
return DOC.TRANSPARENT_HUGEPAGES;
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
this.hugepages = hash.find((node) => {
return node.id === this.node.id;
}) || {};
},
data() {
return { hugepages: {} };
},
};
</script>
<template>
<div>
<template v-if="hugepages.status">
<h2>{{ t('harvester.host.hugepages.meminfo') }}</h2>
<div class="row mb-20">
<div class="col span-6">
<LabelValue
:name="t('harvester.host.hugepages.status.anon')"
:value="hugepages.status.meminfo.anonHugePages"
/>
</div>
<div class="col span-6">
<LabelValue
:name="t('harvester.host.hugepages.status.size')"
:value="hugepages.status.meminfo.hugepageSize"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-3">
<LabelValue
:name="t('harvester.host.hugepages.status.total')"
:value="hugepages.status.meminfo.hugePagesTotal"
/>
</div>
<div class="col span-3">
<LabelValue
:name="t('harvester.host.hugepages.status.free')"
:value="hugepages.status.meminfo.hugePagesFree"
/>
</div>
<div class="col span-3">
<LabelValue
:name="t('harvester.host.hugepages.status.rsvd')"
:value="hugepages.status.meminfo.hugePagesRsvd"
/>
</div>
<div class="col span-3">
<LabelValue
:name="t('harvester.host.hugepages.status.surp')"
:value="hugepages.status.meminfo.hugePagesSurp"
/>
</div>
</div>
<div>
<hr class="divider" />
<h3>
<t
k="harvester.host.hugepages.transparent.title"
:raw="true"
:url="docsTransparentHugepagesLink"
/>
</h3>
<div class="row mb-20">
<div class="col span-4">
<LabelValue
:name="t('harvester.host.hugepages.transparent.enabled')"
:value="hugepages.spec.transparent.enabled"
/>
</div>
<div class="col span-4">
<LabelValue
:name="t('harvester.host.hugepages.transparent.shmemEnabled')"
:value="hugepages.spec.transparent.shmemEnabled"
/>
</div>
<div class="col span-4">
<LabelValue
:name="t('harvester.host.hugepages.transparent.defrag')"
:value="hugepages.spec.transparent.defrag"
/>
</div>
</div>
</div>
</template>
</div>
</template>
@@ -27,6 +27,7 @@ import Instance from './VirtualMachineInstance';
import Disk from './HarvesterHostDisk';
import VlanStatus from './VlanStatus';
import HarvesterKsmtuned from './HarvesterKsmtuned.vue';
import HarvesterHugepages from './HarvesterHugepages.vue';
import HarvesterSeeder from './HarvesterSeeder';
const LONGHORN_SYSTEM = 'longhorn-system';
@@ -46,6 +47,7 @@ export default {
VlanStatus,
LabelValue,
HarvesterKsmtuned,
HarvesterHugepages,
Loading,
SortableTable,
HarvesterSeeder,
@@ -209,6 +211,12 @@ export default {
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
},
hasHugepagesSchema() {
const inStore = this.$store.getters['currentProduct'].inStore;
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.HUGEPAGES);
},
hasBlockDevicesSchema() {
return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE);
},
@@ -468,6 +476,16 @@ export default {
/>
</Tab>
<Tab
v-if="hasHugepagesSchema"
name="hugepages"
:weight="0"
:show-header="false"
:label="t('harvester.host.tabs.hugepages')"
>
<HarvesterHugepages :node="value" />
</Tab>
<Tab
v-if="seederEnabled"
name="seeder"
@@ -75,7 +75,7 @@ export default {
<div class="row">
<div class="col span-6 mb-20">
<LabelValue
:name="t('harvester.schedule.cron')"
:name="t('harvester.schedule.cron.label')"
:value="cronExpression"
/>
</div>
File diff suppressed because it is too large Load Diff
@@ -213,6 +213,7 @@ export default {
const diskRows = this.getDiskRows(neu);
this['diskRows'] = diskRows;
this['networkRows'] = this.getNetworkRows(neu, { fromTemplate: false, init: false });
},
deep: true
}
@@ -265,6 +266,7 @@ export default {
<Network
v-model:value="networkRows"
mode="view"
:vm="value"
/>
</Tab>
@@ -32,7 +32,7 @@ export default {
resources: {
type: Array,
required: true
}
},
},
data() {
@@ -43,7 +43,7 @@ export default {
...mapState('action-menu', ['modalData']),
title() {
return this.modalData.title || 'dialog.promptRemove.title';
return this.modalData?.title || 'dialog.promptRemove.title';
},
formattedType() {
@@ -51,7 +51,7 @@ export default {
},
warningMessage() {
if (this.modalData.warningMessage) return this.modalData.warningMessage;
if (this.modalData?.warningMessage) return this.modalData.warningMessage;
const isPlural = this.type.endsWith('s');
const thisOrThese = isPlural ? 'these' : 'this';
@@ -145,6 +145,7 @@ export default {
try {
for (const resource of this.resources) {
await resource.remove();
if (this.modalData?.extraActionAfterRemove) await this.modalData.extraActionAfterRemove();
}
buttonDone(true);
this.close();
@@ -0,0 +1,212 @@
<script>
import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex';
import { NETWORK_ATTACHMENT } from '@shell/config/types';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { NETWORK_TYPE } from '../config/types';
export default {
name: 'AddHotplugNic',
emits: ['close'],
components: {
AsyncButton,
Card,
LabeledInput,
LabeledSelect,
Banner
},
props: {
resources: {
type: Array,
required: true
}
},
async fetch() {
try {
this.allVMNetworks = await this.$store.dispatch('harvester/findAll', { type: NETWORK_ATTACHMENT });
} catch (err) {
this.errors = exceptionToErrorsArray(err);
this.allVMNetworks = [];
}
},
data() {
return {
interfaceName: '',
networkName: '',
macAddress: '',
allVMNetworks: [],
errors: [],
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources?.[0];
},
isFormValid() {
return this.interfaceName !== '' && this.networkName !== '';
},
vmNetworksOption() {
return this.allVMNetworks
.filter((network) => {
const labels = network.metadata?.labels || {};
const type = labels[HCI_ANNOTATIONS.NETWORK_TYPE];
const isValidType = [
NETWORK_TYPE.L2VLAN,
NETWORK_TYPE.UNTAGGED,
NETWORK_TYPE.L2TRUNK_VLAN,
].includes(type);
return isValidType && !network.isSystem;
})
.map((network) => {
const label = network.isNotReady ? `${ network.id } (${ this.t('generic.notReady') })` : network.id;
return ({
label,
value: network.id || '',
disabled: network.isNotReady,
});
});
}
},
methods: {
close() {
this.interfaceName = '';
this.networkName = '';
this.macAddress = '';
this.errors = [];
this.$emit('close');
},
async save(buttonCb) {
if (!this.actionResource) {
buttonCb(false);
return;
}
const payload = {
interfaceName: this.interfaceName,
networkName: this.networkName
};
if (this.macAddress) {
payload.macAddress = this.macAddress;
}
try {
const res = await this.actionResource.doAction('addNic', payload);
if ([200, 204].includes(res?._status)) {
this.$store.dispatch('growl/success', {
title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.modal.hotplugNic.success', {
interfaceName: this.interfaceName,
vm: this.actionResource.nameDisplay
})
}, { root: true });
this.close();
buttonCb(true);
} else {
this.errors = exceptionToErrorsArray(res);
buttonCb(false);
}
} catch (err) {
this.errors = exceptionToErrorsArray(err);
buttonCb(false);
}
}
}
};
</script>
<template>
<Card
ref="modal"
name="modal"
:show-highlight-border="false"
>
<template #title>
<h4
v-clean-html="t('harvester.modal.hotplugNic.title')"
class="text-default-text"
/>
</template>
<template #body>
<LabeledInput
v-model:value="interfaceName"
:label="t('generic.name')"
required
/>
<LabeledSelect
v-model:value="networkName"
class="mt-20"
:label="t('harvester.modal.hotplugNic.vmNetwork')"
:options="vmNetworksOption"
required
/>
<LabeledInput
v-model:value="macAddress"
class="mt-20"
label-key="harvester.modal.hotplugNic.macAddress"
:tooltip="t('harvester.modal.hotplugNic.macAddressTooltip', _, true)"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
:label="err"
color="error"
/>
</template>
<template #actions>
<div class="actions">
<div class="buttons">
<button
type="button"
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="apply"
:disabled="!isFormValid"
@click="save"
/>
</div>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -11,7 +11,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default {
name: 'HotplugModal',
name: 'HotplugVolumeModal',
emits: ['close'],
@@ -62,7 +62,7 @@ export default {
return false;
}
return !pvc.attachVM;
return true;
})
.map((pvc) => {
return {
@@ -90,7 +90,7 @@ export default {
if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', {
title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.modal.hotplug.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
message: this.t('harvester.modal.hotplugVolume.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
}, { root: true });
this.close();
@@ -122,7 +122,7 @@ export default {
>
<template #title>
<h4
v-clean-html="t('harvester.modal.hotplug.title')"
v-clean-html="t('harvester.modal.hotplugVolume.title')"
class="text-default-text"
/>
</template>
@@ -0,0 +1,183 @@
<script>
import { mapGetters } from 'vuex';
import { STORAGE_CLASS } from '@shell/config/types';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { sortBy } from '@shell/utils/sort';
import { isInternalStorageClass } from '../utils/storage-class';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import { LabeledInput } from '@components/Form/LabeledInput';
import AsyncButton from '@shell/components/AsyncButton';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default {
name: 'HarvesterDataMigrationDialog',
emits: ['close'],
components: {
AsyncButton, Banner, Card, LabeledInput, LabeledSelect
},
props: {
resources: {
type: Array,
required: true
}
},
async fetch() {
this.storageClasses = await this.$store.dispatch('harvester/findAll', { type: STORAGE_CLASS });
},
data() {
return {
targetVolumeName: '',
targetStorageClassName: '',
errors: [],
storageClasses: [],
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0];
},
storageClassOptions() {
return sortBy(
this.storageClasses
.filter((sc) => !isInternalStorageClass(sc.metadata?.name))
.map((sc) => ({
label: sc.metadata?.name,
value: sc.metadata?.name
})),
'label'
);
},
disableSave() {
return !this.targetVolumeName || !this.targetStorageClassName;
},
},
methods: {
close() {
this.targetVolumeName = '';
this.targetStorageClassName = '';
this.errors = [];
this.$emit('close');
},
async apply(buttonDone) {
if (!this.actionResource) {
buttonDone(false);
return;
}
if (!this.targetVolumeName) {
const name = this.t('harvester.modal.dataMigration.fields.targetVolumeName.label');
this['errors'] = [this.t('validation.required', { key: name })];
buttonDone(false);
return;
}
if (!this.targetStorageClassName) {
const name = this.t('harvester.modal.dataMigration.fields.targetStorageClassName.label');
this['errors'] = [this.t('validation.required', { key: name })];
buttonDone(false);
return;
}
try {
await this.actionResource.doAction('dataMigration', {
targetVolumeName: this.targetVolumeName,
targetStorageClassName: this.targetStorageClassName
}, {}, false);
buttonDone(true);
this.close();
} catch (err) {
const error = err?.data || err;
this['errors'] = exceptionToErrorsArray(error);
buttonDone(false);
}
},
}
};
</script>
<template>
<Card :show-highlight-border="false">
<template #title>
{{ t('harvester.modal.dataMigration.title') }}
</template>
<template #body>
<LabeledInput
v-model:value="targetVolumeName"
:label="t('harvester.modal.dataMigration.fields.targetVolumeName.label')"
:placeholder="t('harvester.modal.dataMigration.fields.targetVolumeName.placeholder')"
class="mb-20"
required
/>
<LabeledSelect
v-model:value="targetStorageClassName"
:label="t('harvester.modal.dataMigration.fields.targetStorageClassName.label')"
:placeholder="t('harvester.modal.dataMigration.fields.targetStorageClassName.placeholder')"
:options="storageClassOptions"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template
#actions
class="actions"
>
<div class="buttons">
<button
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="apply"
:disabled="disableSave"
@click="apply"
/>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -1,13 +1,16 @@
<script>
import { mapState, mapGetters } from 'vuex';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
const VOLUME = 'volume';
const NETWORK = 'network';
const CDROM = 'cdrom';
export default {
name: 'HarvesterHotUnplugModal',
name: 'HarvesterHotUnplug',
emits: ['close'],
@@ -35,8 +38,43 @@ export default {
actionResource() {
return this.resources[0];
},
diskName() {
return this.modalData.diskName;
name() {
return this.modalData.name;
},
isVolume() {
return this.modalData.type === VOLUME;
},
titleKey() {
const keys = {
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.title',
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.title',
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.title',
};
return keys[this.modalData.type];
},
actionLabelKey() {
const keys = {
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabels',
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.actionLabels',
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.actionLabels',
};
return keys[this.modalData.type];
},
successMessageKey() {
const keys = {
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.success',
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.success',
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.success',
};
return keys[this.modalData.type];
}
},
@@ -47,14 +85,22 @@ export default {
async save(buttonCb) {
try {
const res = await this.actionResource.doAction('removeVolume', { diskName: this.diskName });
let res;
if (this.modalData.type === VOLUME) {
res = await this.actionResource.doAction('removeVolume', { diskName: this.name });
} else if (this.modalData.type === NETWORK) {
res = await this.actionResource.doAction('removeNic', { interfaceName: this.name });
} else {
res = await this.actionResource.doAction('ejectCdRomVolume', { deviceName: this.name });
}
if (res._status === 200 || res._status === 204) {
this.$store.dispatch(
'growl/success',
{
title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.modal.hotunplug.success', { name: this.diskName })
message: this.t(this.successMessageKey, { name: this.name })
},
{ root: true }
);
@@ -64,14 +110,14 @@ export default {
} else {
const error = [res?.data] || exceptionToErrorsArray(res);
this['errors'] = error;
this.errors = error;
buttonCb(false);
}
} catch (err) {
const error = err?.data || err;
const message = exceptionToErrorsArray(error);
this['errors'] = message;
this.errors = message;
buttonCb(false);
}
}
@@ -87,7 +133,7 @@ export default {
>
<template #title>
<h4
v-clean-html="t('harvester.virtualMachine.unplug.title', { name: diskName })"
v-clean-html="t(titleKey, { name })"
class="text-default-text"
/>
<Banner
@@ -111,9 +157,9 @@ export default {
<AsyncButton
mode="apply"
:action-label="t('harvester.virtualMachine.unplug.actionLabel')"
:waiting-label="t('harvester.virtualMachine.unplug.actionLabel')"
:success-label="t('harvester.virtualMachine.unplug.actionLabel')"
:action-label="t(actionLabelKey)"
:waiting-label="t(actionLabelKey)"
:success-label="t(actionLabelKey)"
@click="save"
/>
</div>
@@ -132,4 +178,8 @@ export default {
justify-content: flex-end;
width: 100%;
}
::v-deep(.card-title) {
display: block;
}
</style>
@@ -0,0 +1,198 @@
<script>
import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapState, mapGetters } from 'vuex';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { HCI } from '../types';
export default {
name: 'HarvesterInsertCdRomVolume',
emits: ['close'],
components: {
AsyncButton,
Card,
LabeledInput,
LabeledSelect,
Banner
},
props: {
resources: {
type: Array,
required: true
}
},
async fetch() {
try {
this.images = await this.$store.dispatch('harvester/findAll', { type: HCI.IMAGE });
} catch (err) {
this.errors = exceptionToErrorsArray(err);
this.images = [];
}
},
data() {
return {
imageName: '',
images: [],
errors: [],
};
},
computed: {
...mapState('action-menu', ['modalData']),
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources?.[0];
},
isFormValid() {
return this.imageName !== '';
},
deviceName() {
return this.modalData.name;
},
imagesOption() {
return this.images
.filter((image) => {
const labels = image.metadata?.labels || {};
const type = labels[HCI_ANNOTATIONS.IMAGE_SUFFIX];
return type === 'iso';
})
.map((image) => {
return ({
label: this.imageOptionLabel(image),
value: image.id,
disabled: image.isImportedImage
});
});
}
},
methods: {
close() {
this.imageName = '';
this.errors = [];
this.$emit('close');
},
imageOptionLabel(image) {
return `${ image.metadata.namespace }/${ image.spec.displayName }`;
},
async save(buttonCb) {
if (!this.actionResource) {
buttonCb(false);
return;
}
const payload = {
deviceName: this.deviceName,
imageName: this.imageName
};
try {
const res = await this.actionResource.doAction('insertCdRomVolume', payload);
if ([200, 204].includes(res?._status)) {
this.$store.dispatch('growl/success', {
title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.modal.insertCdRomVolume.success', {
deviceName: this.deviceName,
imageName: this.imageName,
})
}, { root: true });
this.close();
buttonCb(true);
} else {
this.errors = exceptionToErrorsArray(res);
buttonCb(false);
}
} catch (err) {
this.errors = exceptionToErrorsArray(err);
buttonCb(false);
}
}
}
};
</script>
<template>
<Card
ref="modal"
name="modal"
:show-highlight-border="false"
>
<template #title>
<h4
v-clean-html="t('harvester.modal.insertCdRomVolume.title')"
class="text-default-text"
/>
</template>
<template #body>
<LabeledInput
v-model:value="deviceName"
:label="t('generic.name')"
disabled
/>
<LabeledSelect
v-model:value="imageName"
class="mt-20"
:label="t('harvester.modal.insertCdRomVolume.image')"
:options="imagesOption"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
:label="err"
color="error"
/>
</template>
<template #actions>
<div class="actions">
<div class="buttons">
<button
type="button"
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="apply"
:disabled="!isFormValid"
@click="save"
/>
</div>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -0,0 +1,239 @@
<script>
import { mapGetters } from 'vuex';
import { PVC } from '@shell/config/types';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { sortBy } from '@shell/utils/sort';
import { HCI } from '../types';
import { parseVolumeClaimTemplates } from '@pkg/harvester/utils/vm';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default {
name: 'HarvesterStorageMigrationDialog',
emits: ['close'],
components: {
AsyncButton, Banner, Card, LabeledSelect
},
props: {
resources: {
type: Array,
required: true
}
},
async fetch() {
this.allPVCs = await this.$store.dispatch('harvester/findAll', { type: PVC });
},
data() {
return {
sourceVolume: '',
targetVolume: '',
errors: [],
allPVCs: [],
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0];
},
sourceVolumeOptions() {
const volumes = this.actionResource.spec?.template?.spec?.volumes || [];
return sortBy(
volumes
.map((v) => v.persistentVolumeClaim?.claimName)
.filter((name) => !!name)
.map((name) => ({
label: name,
value: name
})),
'label'
);
},
namespacePVCs() {
return this.allPVCs.filter((pvc) => pvc.metadata.namespace === this.actionResource.metadata.namespace);
},
vmUsedVolumeNames() {
const allVMs = this.$store.getters['harvester/all'](HCI.VM) || [];
const names = new Set();
allVMs.forEach((vm) => {
// Collect volume names from spec.template.spec.volumes (both PVC and DataVolume references)
const volumes = vm.spec?.template?.spec?.volumes || [];
volumes.forEach((v) => {
const name = v.persistentVolumeClaim?.claimName || v.dataVolume?.name;
if (name) {
names.add(`${ vm.metadata.namespace }/${ name }`);
}
});
// Collect volume names from volumeClaimTemplates annotation
const templates = parseVolumeClaimTemplates(vm);
templates.forEach((t) => {
if (t.metadata?.name) {
names.add(`${ vm.metadata.namespace }/${ t.metadata.name }`);
}
});
});
return names;
},
targetVolumeOptions() {
return sortBy(
this.namespacePVCs
.filter((pvc) => {
// Exclude volumes used by any VM (via spec.volumes or volumeClaimTemplates)
if (this.vmUsedVolumeNames.has(`${ pvc.metadata.namespace }/${ pvc.metadata.name }`)) {
return false;
}
return true;
})
.map((pvc) => ({
label: pvc.metadata.name,
value: pvc.metadata.name
})),
'label'
);
},
disableSave() {
return !this.sourceVolume || !this.targetVolume;
},
},
methods: {
close() {
this.sourceVolume = '';
this.targetVolume = '';
this.errors = [];
this.$emit('close');
},
async apply(buttonDone) {
if (!this.actionResource) {
buttonDone(false);
return;
}
if (!this.sourceVolume) {
const name = this.t('harvester.modal.storageMigration.fields.sourceVolume.label');
this['errors'] = [this.t('validation.required', { key: name })];
buttonDone(false);
return;
}
if (!this.targetVolume) {
const name = this.t('harvester.modal.storageMigration.fields.targetVolume.label');
this['errors'] = [this.t('validation.required', { key: name })];
buttonDone(false);
return;
}
try {
await this.actionResource.doAction('storageMigration', {
sourceVolume: this.sourceVolume,
targetVolume: this.targetVolume
}, {}, false);
buttonDone(true);
this.close();
} catch (err) {
const error = err?.data || err;
this['errors'] = exceptionToErrorsArray(error);
buttonDone(false);
}
},
}
};
</script>
<template>
<Card :show-highlight-border="false">
<template #title>
{{ t('harvester.modal.storageMigration.title') }}
</template>
<template #body>
<LabeledSelect
v-model:value="sourceVolume"
:label="t('harvester.modal.storageMigration.fields.sourceVolume.label')"
:placeholder="t('harvester.modal.storageMigration.fields.sourceVolume.placeholder')"
:options="sourceVolumeOptions"
class="mb-20"
required
/>
<LabeledSelect
v-model:value="targetVolume"
:label="t('harvester.modal.storageMigration.fields.targetVolume.label')"
:placeholder="t('harvester.modal.storageMigration.fields.targetVolume.placeholder')"
:options="targetVolumeOptions"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template
#actions
class="actions"
>
<div class="buttons">
<button
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="apply"
:disabled="disableSave"
@click="apply"
/>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -277,8 +277,9 @@ export default {
:label="t('harvester.modal.bundle.namespaces.label')"
:clearable="true"
:multiple="true"
:append-to-body="false"
:options="namespaceOptions"
class="mb-10 label-select"
class="mb-10 namespace-select"
:tooltip="t('harvester.modal.bundle.namespaces.tooltip', _, true)"
@update:value="updateNamespaces"
/>
@@ -372,6 +373,11 @@ export default {
padding: 10px 0;
height: 160px;
}
.namespace-select {
:deep(.vs__dropdown-menu) {
max-height: 210px;
}
}
}
div {
@@ -0,0 +1,133 @@
<script>
import Tabbed from '@shell/components/Tabbed';
import Tab from '@shell/components/Tabbed/Tab';
import CruResource from '@shell/components/CruResource';
import { LabeledInput } from '@components/Form/LabeledInput';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import LabelValue from '@shell/components/LabelValue';
import CreateEditView from '@shell/mixins/create-edit-view';
export default {
name: 'HarvesterEditMIGConfiguration',
components: {
Tab,
Tabbed,
CruResource,
LabeledInput,
NameNsDescription,
LabelValue
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
const { profileSpec } = this.value.spec;
return { profileSpec: profileSpec || [] };
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
isView() {
return this.mode === 'view';
},
},
methods: {
updateBeforeSave() {
// MIGConfiguration CRD don't have any namespace field,
// so we need to remove the namespace field before saving
delete this.value.metadata.namespace;
// enable the MIGConfiguration when saving
this.value.spec.enabled = true;
},
labelTitle(profile) {
return `${ profile.name } (available : ${ this.available(profile) })`;
},
available(profile) {
const count = this.value.status?.profileStatus?.find((p) => p.id === profile.id)?.available;
return count || 0;
},
updateRequested(neu, profile) {
if (neu === null || neu === '') return;
const newValue = Number(neu);
const availableCount = this.available(profile);
if (newValue < 0) {
profile.requested = 0;
} else if ( newValue > availableCount ) {
profile.requested = availableCount;
} else {
profile.requested = newValue;
}
}
},
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
finish-button-mode="enable"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
:mode="mode"
/>
<Tabbed
v-bind="$attrs"
class="mt-15"
:side-tabs="true"
>
<Tab
name="profileSpec"
:label="t('harvester.migconfiguration.profileSpec')"
:weight="1"
class="bordered-table"
>
<div
v-for="(profile, index) in profileSpec"
:key="index"
>
<LabelValue
:value="labelTitle(profile)"
class="mb-10"
/>
<LabeledInput
v-model:value="profile.requested"
:min="0"
:disabled="isView"
type="number"
class="mb-20"
:label="`${t('harvester.migconfiguration.requested')}`"
@update:value="updateRequested($event, profile)"
/>
</div>
</Tab>
</Tabbed>
</CruResource>
</template>
@@ -8,7 +8,7 @@ import LabelValue from '@shell/components/LabelValue';
import { BadgeState } from '@components/BadgeState';
import { Banner } from '@components/Banner';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { RadioGroup, RadioButton } from '@components/Form/Radio';
import { RadioGroup } from '@components/Form/Radio';
import HarvesterDisk from '../../mixins/harvester-disk';
import Tags from '../../components/DiskTags';
import { HCI } from '../../types';
@@ -30,7 +30,6 @@ export default {
BadgeState,
Banner,
RadioGroup,
RadioButton,
ModalWithCard,
Tags,
},
@@ -184,12 +183,11 @@ export default {
},
forceFormattedDisabled() {
const lastFormattedAt = this.blockDevice?.status?.deviceStatus?.fileSystem?.LastFormattedAt;
const fileSystem = this.blockDevice?.status?.deviceStatus?.fileSystem.type;
const systems = ['ext4', 'XFS'];
if (lastFormattedAt || this.blockDevice?.childParts?.length > 0) {
if (this.blockDevice?.childParts?.length > 0) {
return true;
} else if (systems.includes(fileSystem)) {
return false;
@@ -446,7 +444,7 @@ export default {
/>
</div>
<div
v-if="(value.isNew && isLonghornV1 && !isFormatted) || isCorrupted"
v-if="(value.isNew && isLonghornV1) || isCorrupted"
class="col span-6"
>
<RadioGroup
@@ -459,15 +457,6 @@ export default {
:disabled="forceFormattedDisabled"
tooltip-key="harvester.host.disk.forceFormatted.toolTip"
>
<template #1="{option, listeners}">
<RadioButton
:label="option.label"
:val="option.value"
:value="value.forceFormatted"
:disabled="forceFormattedDisabled && !value.forceFormatted"
v-on="listeners"
/>
</template>
</RadioGroup>
</div>
<div
@@ -0,0 +1,157 @@
<script>
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
import { HCI } from '../../types';
import { DOC } from '../../config/doc-links';
export const hugepagesTHPEnabledMode = [{
label: 'Always',
value: 'always',
}, {
label: 'Madvise',
value: 'madvise',
}, {
label: 'Never',
value: 'never',
}];
export const hugepagesTHPShmemEnabledMode = [{
label: 'Always',
value: 'always',
}, {
label: 'Within Size',
value: 'within_size',
}, {
label: 'Advise',
value: 'advise',
}, {
label: 'Never',
value: 'never',
}, {
label: 'Deny',
value: 'deny',
}, {
label: 'Force',
value: 'force',
}];
export const hugepagesTHPDefragMode = [{
label: 'Always',
value: 'always',
}, {
label: 'Defer',
value: 'defer',
}, {
label: 'Defer+Madvise',
value: 'defer+madvise',
}, {
label: 'Madvise',
value: 'madvise',
}, {
label: 'Never',
value: 'never'
}];
export default {
name: 'HarvesterHugepages',
components: { LabeledSelect },
props: {
node: {
type: Object,
required: true,
},
registerBeforeHook: {
type: Function,
required: true,
},
},
computed: {
docsTransparentHugepagesLink() {
return DOC.TRANSPARENT_HUGEPAGES;
},
},
methods: {
async saveHugepages() {
this.hugepages['spec'] = this.spec;
await this.hugepages.save().catch((reason) => {
if (reason?.type === 'error') {
this.$store.dispatch('growl/error', {
title: this.t('harvester.notification.title.error'),
message: reason?.message,
}, { root: true });
return Promise.reject(new Error('saveHugepages error'));
}
});
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
this.hugepages = hash.find((node) => {
return node.id === this.node.id;
});
this.spec = this.hugepages.spec;
},
data() {
return {
hugepages: {},
spec: { transparent: {} },
hugepagesTHPEnabledMode,
hugepagesTHPShmemEnabledMode,
hugepagesTHPDefragMode,
};
},
created() {
this.registerBeforeHook(this.saveHugepages, 'saveHugepages');
},
};
</script>
<template>
<div>
<div>
<hr class="divider" />
<h3>
<t
k="harvester.host.hugepages.transparent.title"
:raw="true"
:url="docsTransparentHugepagesLink"
/>
</h3>
<div class="row mb-20">
<div class="col span-4">
<LabeledSelect
v-model:value="spec.transparent.enabled"
:label="t('harvester.host.hugepages.transparent.enabled')"
:options="hugepagesTHPEnabledMode"
/>
</div>
<div class="col span-4">
<LabeledSelect
v-model:value="spec.transparent.shmemEnabled"
:label="t('harvester.host.hugepages.transparent.shmemEnabled')"
:options="hugepagesTHPShmemEnabledMode"
/>
</div>
<div class="col span-4">
<LabeledSelect
v-model:value="spec.transparent.defrag"
:label="t('harvester.host.hugepages.transparent.defrag')"
:options="hugepagesTHPDefragMode"
/>
</div>
</div>
</div>
</div>
</template>
@@ -7,6 +7,8 @@ import { Checkbox } from '@components/Form/Checkbox';
import { HCI } from '../../types';
import { DOC } from '../../config/doc-links';
import { docLink } from '../../utils/feature-flags';
import isEqual from 'lodash/isEqual';
import { clone } from '@shell/utils/object';
export const ksmtunedMode = [{
value: 'standard',
@@ -64,11 +66,13 @@ export default {
this.enableMergeAcrossNodes = !!this.ksmtuned.spec?.mergeAcrossNodes;
this.spec = this.ksmtuned.spec;
this.originSpec = clone(this.ksmtuned.spec);
},
data() {
return {
ksmtuned: {},
originSpec: {},
spec: {},
thresCoef: 30,
ksmtunedMode,
@@ -99,6 +103,11 @@ export default {
methods: {
async saveKsmtuned() {
// no ksmtuned spec change, skip save
if (isEqual(this.spec, this.originSpec)) {
return;
}
this.spec.mergeAcrossNodes = this.enableMergeAcrossNodes ? 1 : 0;
this.ksmtuned['spec'] = this.spec;
@@ -28,6 +28,7 @@ import { HCI } from '../../types';
import HarvesterDisk from './HarvesterDisk';
import HarvesterSeeder from './HarvesterSeeder';
import HarvesterKsmtuned from './HarvesterKsmtuned';
import HarvesterHugepages from './HarvesterHugepages';
import Tags from '../../components/DiskTags';
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
import isEqual from 'lodash/isEqual';
@@ -50,6 +51,7 @@ export default {
ArrayListGrouped,
HarvesterDisk,
HarvesterKsmtuned,
HarvesterHugepages,
ButtonDropdown,
KeyValue,
Banner,
@@ -225,6 +227,12 @@ export default {
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
},
hasHugepagesSchema() {
const inStore = this.$store.getters['currentProduct'].inStore;
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.HUGEPAGES);
},
hasBlockDevicesSchema() {
const inStore = this.$store.getters['currentProduct'].inStore;
@@ -527,7 +535,6 @@ export default {
id: this.longhornNode.id,
opt: { force: true },
});
await new Promise((resolve) => setTimeout(resolve, '5000'));
await retrySave();
} else {
@@ -648,6 +655,17 @@ export default {
</template>
</ArrayListGrouped>
</Tab>
<Tab
v-if="hasHugepagesSchema"
name="Hugepages"
:weight="70"
:label="t('harvester.host.tabs.hugepages')"
>
<HarvesterHugepages
:node="value"
:register-before-hook="registerBeforeHook"
/>
</Tab>
<Tab
v-if="hasKsmtunedSchema"
name="Ksmtuned"
@@ -10,9 +10,13 @@ import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-anno
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../types';
import { NETWORK_TYPE } from '../config/types';
import { NETWORK_TYPE, L2VLAN_MODE } from '../config/types';
import { removeObject } from '@shell/utils/array';
const { L2VLAN, UNTAGGED, OVERLAY } = NETWORK_TYPE;
const {
L2VLAN, UNTAGGED, OVERLAY, L2TRUNK_VLAN
} = NETWORK_TYPE;
const { ACCESS, TRUNK } = L2VLAN_MODE;
const AUTO = 'auto';
const MANUAL = 'manual';
@@ -52,6 +56,8 @@ export default {
return {
config,
type,
l2VlanMode: this.value.vlanType === L2TRUNK_VLAN ? TRUNK : ACCESS,
vlanTrunk: this.parseVlanTrunk(config),
layer3Network: {
mode: layer3Network.mode || AUTO,
serverIPAddr: layer3Network.serverIPAddr || '',
@@ -108,6 +114,10 @@ export default {
}];
},
l2VlanTrunkModeFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('l2VlanTrunkMode');
},
kubeovnVpcSubnetSupport() {
return this.$store.getters['harvester-common/getFeatureEnabled']('kubeovnVpcSubnet');
},
@@ -131,6 +141,16 @@ export default {
});
},
l2VlanModeOptions() {
return [{
label: this.t('harvester.vlanStatus.vlanConfig.l2VlanMode.access'),
value: ACCESS,
}, {
label: this.t('harvester.vlanStatus.vlanConfig.l2VlanMode.trunk'),
value: TRUNK,
}];
},
networkTypes() {
const types = [L2VLAN, UNTAGGED];
@@ -149,6 +169,22 @@ export default {
return this.type === L2VLAN;
},
isL2VlanTrunkMode() {
if (this.isView) {
return this.value.vlanType === L2TRUNK_VLAN && this.l2VlanMode === TRUNK;
}
return this.type === L2TRUNK_VLAN || (this.l2VlanMode === TRUNK && this.type === L2VLAN);
},
isL2VlanAccessMode() {
if (this.isView) {
return this.value.vlanType === L2VLAN && this.l2VlanMode === ACCESS;
}
return this.type === L2VLAN && this.l2VlanMode === ACCESS;
},
isOverlayNetwork() {
if (this.isView) {
return this.value.vlanType === OVERLAY;
@@ -168,11 +204,11 @@ export default {
watch: {
type(newType) {
if (newType === OVERLAY) {
if (newType === OVERLAY) { // overlay network configuration
this.config.type = 'kube-ovn';
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
this.config.server_socket = '/run/openvswitch/kube-ovn-daemon.sock';
} else {
} else { // l2vlan or untagged network configuration
this.config.type = 'bridge';
this.config.promiscMode = true;
this.config.ipam = {};
@@ -180,7 +216,20 @@ export default {
delete this.config.provider;
delete this.config.server_socket;
}
}
},
l2VlanMode(newAccessMode) {
if (this.type !== L2VLAN) {
return;
}
if (newAccessMode === TRUNK) { // trunk mode
this.config.vlan = 0;
this.config.vlanTrunk = this.vlanTrunk;
} else { // access mode
delete this.config.vlanTrunk;
this.config.vlan = '';
}
},
},
methods: {
@@ -188,7 +237,15 @@ export default {
const errors = [];
if (this.isL2VlanNetwork || this.isUntaggedNetwork) {
if (!this.config.vlan && !this.isUntaggedNetwork) {
if (this.isL2VlanTrunkMode && this.vlanTrunk.some((trunk) => trunk.minID === '')) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.vlanStatus.vlanConfig.vlanTrunk.minId') }));
}
if (this.isL2VlanTrunkMode && this.vlanTrunk.some((trunk) => trunk.maxID === '')) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.vlanStatus.vlanConfig.vlanTrunk.maxId') }));
}
if (this.isL2VlanAccessMode && !this.config.vlan && !this.isUntaggedNetwork) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
}
@@ -217,6 +274,30 @@ export default {
await this.save(buttonCb);
},
parseVlanTrunk(config) {
if (config?.vlanTrunk && config?.vlanTrunk?.length > 0) {
return config.vlanTrunk;
}
return [{ minID: '', maxID: '' }];
},
removeVlanTrunk(trunk) {
removeObject(this.vlanTrunk, trunk);
},
addVlanTrunk() {
if (!this.config.vlanTrunk) {
this.config.vlanTrunk = [];
}
this.vlanTrunk.push({ minID: this.minId, maxID: this.maxId });
this.config.vlanTrunk = this.vlanTrunk;
},
vlanTrunkChange() {
this.config.vlanTrunk = this.vlanTrunk;
},
input(neu) {
if (neu === '') {
this.config.vlan = '';
@@ -292,8 +373,76 @@ export default {
required
/>
<LabeledSelect
v-if="isL2VlanNetwork && l2VlanTrunkModeFeatureEnabled"
v-model:value="l2VlanMode"
class="mb-20"
:options="l2VlanModeOptions"
:mode="mode"
:disabled="isEdit"
:label="t('harvester.vlanStatus.vlanConfig.l2VlanMode.label')"
required
/>
<div v-if="isL2VlanTrunkMode && l2VlanTrunkModeFeatureEnabled">
<div
v-for="(trunk, i) in vlanTrunk"
:key="i"
>
<div class="row mt-10">
<div class="col trunk-span">
<LabeledInput
v-model:value.number="trunk.minID"
class="mb-20"
required
placeholder="e.g. 1-4094"
:min="1"
:max="4094"
type="number"
:label="t('harvester.vlanStatus.vlanConfig.vlanTrunk.minId')"
:mode="mode"
@update:value="vlanTrunkChange"
/>
</div>
<div class="col trunk-span">
<LabeledInput
v-model:value.number="trunk.maxID"
class="mb-20"
:max="4094"
:min="1"
placeholder="e.g. 1-4094"
required
type="number"
:label="t('harvester.vlanStatus.vlanConfig.vlanTrunk.maxId')"
:mode="mode"
@update:value="vlanTrunkChange"
/>
</div>
<div class="col remove-btn mb-20">
<button
type="button"
:disabled="isView || vlanTrunk.length <= 1"
:aria-label="t('generic.ariaLabel.remove', {index: i+1})"
role="button"
class="btn role-link"
@click="removeVlanTrunk(trunk)"
>
{{ t('harvester.fields.remove') }}
</button>
</div>
</div>
</div>
<button
v-if="isL2VlanTrunkMode"
type="button"
class="btn btn-sm bg-primary mb-20"
:disabled="isView"
@click="addVlanTrunk"
>
{{ t('harvester.vlanStatus.vlanConfig.vlanTrunk.add') }}
</button>
</div>
<LabeledInput
v-if="isL2VlanNetwork"
v-if="isL2VlanAccessMode"
v-model:value.number="config.vlan"
class="mb-20"
required
@@ -316,7 +465,7 @@ export default {
/>
</Tab>
<Tab
v-if="isL2VlanNetwork"
v-if="isL2VlanAccessMode"
name="layer3Network"
:label="t('harvester.network.tabs.layer3Network')"
:weight="98"
@@ -375,3 +524,12 @@ export default {
</Tabbed>
</CruResource>
</template>
<style lang="scss" scoped>
.remove-btn {
align-self: center;
}
.trunk-span{
flex: 5;
}
</style>
@@ -1,4 +1,5 @@
<script>
import { mapGetters } from 'vuex';
import { RadioGroup } from '@components/Form/Radio';
import { Banner } from '@components/Banner';
import { LabeledInput } from '@components/Form/LabeledInput';
@@ -15,6 +16,8 @@ import { HCI } from '../types';
import { sortBy } from '@shell/utils/sort';
import { BACKUP_TYPE } from '../config/types';
import { _EDIT, _CREATE } from '@shell/config/query-params';
import { isBackupTargetSettingEmpty, isBackupTargetSettingUnavailable } from '../utils/setting';
import CronExpressionEditorModal from '@shell/components/Cron/CronExpressionEditorModal.vue';
export default {
name: 'CreateVMSchedule',
@@ -27,6 +30,7 @@ export default {
LabeledSelect,
MessageLink,
Banner,
CronExpressionEditorModal
},
mixins: [CreateEditView],
@@ -85,15 +89,17 @@ export default {
}
}
return { settings: [] };
return { settings: [], showModel: false };
},
computed: {
...mapGetters({ t: 'i18n/t' }),
backupTargetResource() {
return this.settings.find( (O) => O.id === 'backup-target');
},
isEmptyValue() {
return this.getBackupTargetValueIsEmpty(this.backupTargetResource);
return isBackupTargetSettingEmpty(this.backupTargetResource);
},
canUpdate() {
return this?.backupTargetResource?.canUpdate;
@@ -109,7 +115,7 @@ export default {
!!this.value.spec.maxFailure;
},
isBackupTargetUnAvailable() {
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && (this.errorMessage || this.isEmptyValue) && this.canUpdate;
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && isBackupTargetSettingUnavailable(this.backupTargetResource);
},
vmOptions() {
const nsVmList = this.$store.getters['harvester/all'](HCI.VM).filter((vm) => vm.metadata.namespace === this.value.metadata.namespace);
@@ -166,25 +172,14 @@ export default {
onTypeChange(newType) {
this.value.metadata.name = `svm${ newType }-${ this.value.spec.vmbackup.source.name }`;
},
getBackupTargetValueIsEmpty(resource) {
let out = true;
if (resource?.value) {
try {
const valueJson = JSON.parse(resource?.value);
out = !valueJson.type;
} catch (e) {}
}
return out;
},
validateFailure(count) {
if (this.value.spec.retain && count > this.value.spec.retain) {
this.value.spec['maxFailure'] = this.value.spec.retain;
}
},
openModal() {
this.showModel = true;
},
},
};
</script>
@@ -269,16 +264,28 @@ export default {
:weight="99"
class="bordered-table"
>
<LabeledInput
v-model:value="value.spec.cron"
class="mb-30"
type="cron"
required
:mode="mode"
:label="t('harvester.schedule.cron')"
placeholder="0 * * * *"
:disabled="isBackupTargetUnAvailable || isView"
/>
<div class="cronEditor">
<LabeledInput
v-model:value="value.spec.cron"
class="mb-30"
type="cron"
required
:mode="mode"
:label="t('harvester.schedule.cron.label')"
placeholder="0 * * * *"
:disabled="isBackupTargetUnAvailable || isView"
/>
<button
class="editCronBtn btn role-primary"
@click="openModal"
>
{{ t('harvester.schedule.cron.editButton') }}
</button>
<CronExpressionEditorModal
v-model:show="showModel"
v-model:cron-expression="value.spec.cron"
/>
</div>
<LabeledInput
v-model:value.number="value.spec.retain"
class="mb-30"
@@ -305,3 +312,16 @@ export default {
</Tabbed>
</CruResource>
</template>
<style lang="scss" scoped>
.cronEditor {
align-items: center;
display: flex;
}
.editCronBtn {
margin-bottom: 30px;
margin-left: 10px;
height: 60px;
}
</style>
@@ -296,4 +296,9 @@ export default {
:deep() .edit-help code {
padding: 1px 5px;
}
::v-deep(.banner__content.closable) {
white-space: normal;
word-break: break-word;
}
</style>
@@ -6,6 +6,7 @@ import { Checkbox } from '@components/Form/Checkbox';
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { set } from '@shell/utils/object';
import { Banner } from '@components/Banner';
import KeyValue from '@shell/components/form/KeyValue';
import NodeScheduling from '@shell/components/form/NodeScheduling';
@@ -22,6 +23,7 @@ import Reserved from './kubevirt.io.virtualmachine/VirtualMachineReserved';
import Volume from './kubevirt.io.virtualmachine/VirtualMachineVolume';
import Network from './kubevirt.io.virtualmachine/VirtualMachineNetwork';
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';
@@ -38,6 +40,7 @@ export default {
Network,
Checkbox,
CpuMemory,
CpuModel,
CruResource,
CloudConfig,
LabeledSelect,
@@ -70,12 +73,12 @@ export default {
return {
templateId,
templateValue: null,
templateSpec: null,
versionName: '',
description: '',
defaultVersion: null,
isDefaultVersion: false,
templateValue: null,
templateSpec: null,
versionName: '',
description: '',
defaultVersion: null,
isDefaultVersion: false,
};
},
@@ -154,6 +157,18 @@ export default {
},
methods: {
updateCpuModel(value) {
if (!this.spec?.template?.spec?.domain?.cpu) {
set(this.spec, 'template.spec.domain.cpu', {});
}
if (value && value !== '') {
set(this.spec.template.spec.domain.cpu, 'model', value);
} else {
delete this.spec.template.spec.domain.cpu.model;
}
},
async saveVMT(buttonCb) {
this.parseVM();
@@ -436,6 +451,17 @@ export default {
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<CpuModel
:value="spec.template.spec.domain.cpu?.model || ''"
:mode="mode"
@update:value="updateCpuModel"
/>
</div>
</div>
<div class="row mb-20">
<a
v-if="showAdvanced"
+161 -20
View File
@@ -7,9 +7,13 @@ import ResourceTabs from '@shell/components/form/ResourceTabs';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import Conditions from '@shell/components/form/Conditions';
import { Banner } from '@components/Banner';
import { Checkbox } from '@components/Form/Checkbox';
import jsyaml from 'js-yaml';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { allHash } from '@shell/utils/promise';
import { get } from '@shell/utils/object';
import { clone, get } from '@shell/utils/object';
import { STORAGE_CLASS, LONGHORN, PV } from '@shell/config/types';
import { sortBy } from '@shell/utils/sort';
import { saferDump } from '@shell/utils/create-yaml';
@@ -32,6 +36,7 @@ export default {
components: {
Banner,
Checkbox,
Tab,
UnitInput,
CruResource,
@@ -40,6 +45,7 @@ export default {
LabeledSelect,
LabeledInput,
NameNsDescription,
Conditions
},
mixins: [CreateEditView],
@@ -88,14 +94,34 @@ export default {
source,
storage,
imageId,
snapshots: [],
images: [],
showAdvanced: false,
createWithDataVolume: false,
snapshots: [],
images: [],
GIBIBYTE
};
},
created() {
this.registerBeforeHook(this.willSave, 'willSave');
if (this.mode === _CREATE) {
const originalSaveYaml = this.value.saveYaml?.bind(this.value);
this.value.saveYaml = async(yaml) => {
if (this.createWithDataVolume && this.isBlank) {
const parsed = jsyaml.load(yaml);
const dvObj = { ...parsed, type: 'cdi.kubevirt.io.datavolume' };
const dataVolume = await this.$store.dispatch('harvester/create', dvObj);
await dataVolume.save();
return dataVolume;
}
return originalSaveYaml(yaml);
};
}
},
computed: {
@@ -133,6 +159,10 @@ export default {
return Object.values(VOLUME_MODE);
},
accessModeOptions() {
return ['ReadWriteOnce', 'ReadWriteMany', 'ReadOnlyMany'];
},
imageOption() {
return sortBy(
this.images
@@ -267,7 +297,23 @@ export default {
rebuildStatus() {
return this.value.longhornEngine?.status?.rebuildStatus;
}
},
isLHV2VolExpansionFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
},
isCreatePVCWithDataVolumeFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('createPVCWithDataVolume');
},
isResizeDisabled() {
return (
!this.isLHV2VolExpansionFeatureEnabled &&
this.value?.isLonghornV2 &&
this.isEdit
);
},
},
watch: {
@@ -327,6 +373,58 @@ export default {
return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany'];
},
buildDataVolumeObj() {
const storage = {
storageClassName: this.value.spec.storageClassName,
resources: { requests: { storage: this.storage } },
};
if (this.showAdvanced && this.value.spec.accessModes?.length > 0) {
storage.accessModes = this.value.spec.accessModes;
}
if (this.showAdvanced && this.value.spec.volumeMode) {
storage.volumeMode = this.value.spec.volumeMode;
}
return {
type: 'cdi.kubevirt.io.datavolume',
apiVersion: 'cdi.kubevirt.io/v1beta1',
kind: 'DataVolume',
metadata: {
name: this.value.metadata.name,
namespace: this.value.metadata.namespace,
annotations: this.value.metadata.annotations || {},
labels: this.value.metadata.labels || {},
},
spec: {
source: { blank: {} },
storage,
}
};
},
async save(buttonDone) {
if (this.isCreate && this.isBlank && this.createWithDataVolume) {
try {
this.update();
const dvObj = this.buildDataVolumeObj();
const dataVolume = await this.$store.dispatch('harvester/create', dvObj);
await dataVolume.save();
buttonDone(true);
this.done();
} catch (err) {
const error = err?.data || err;
this['errors'] = exceptionToErrorsArray(error);
buttonDone(false);
}
} else {
await CreateEditView.methods.save.call(this, buttonDone);
}
},
willSave() {
this.update();
},
@@ -369,9 +467,17 @@ export default {
this.update();
},
generateYaml() {
const out = saferDump(this.value);
this.update();
return out;
if (this.isCreate && this.isBlank && this.createWithDataVolume) {
return saferDump(this.buildDataVolumeObj());
}
const plain = clone(this.value);
delete plain.saveYaml;
return saferDump(plain);
},
}
};
@@ -444,18 +550,6 @@ export default {
@update:value="update"
/>
<LabeledSelect
v-if="showVolumeMode"
v-model:value="value.spec.volumeMode"
:label="t('harvester.volume.volumeMode')"
:options="volumeModeOptions"
required
:disabled="!isCreate"
:mode="mode"
class="mb-20"
@update:value="update"
/>
<UnitInput
v-model:value="storage"
:label="t('harvester.volume.size')"
@@ -463,7 +557,7 @@ export default {
:output-modifier="true"
:increment="1024"
:mode="mode"
:disabled="value?.isLonghornV2 && isEdit"
:disabled="isResizeDisabled"
required
class="mb-20"
:suffix="GIBIBYTE"
@@ -471,11 +565,49 @@ export default {
/>
<Banner
v-if="value?.isLonghornV2 && isEdit"
v-if="isResizeDisabled"
color="warning"
>
<span>{{ t('harvester.volume.longhorn.disableResize') }}</span>
</Banner>
<div class="row mb-20">
<Checkbox
v-if="isCreate && isBlank && isCreatePVCWithDataVolumeFeatureEnabled"
v-model:value="createWithDataVolume"
:label="t('harvester.volume.createWithDataVolume')"
tooltip-key="harvester.volume.createWithDataVolumeTooltip"
/>
</div>
<a
v-if="isCreate && isCreatePVCWithDataVolumeFeatureEnabled"
role="button"
class="hand"
@click="showAdvanced = !showAdvanced"
>
{{ showAdvanced ? t('harvester.volume.hideAdvanced') : t('harvester.volume.showAdvanced') }}
</a>
<LabeledSelect
v-if="showAdvanced"
v-model:value="value.spec.accessModes"
:label="t('harvester.volume.accessModes')"
:options="accessModeOptions"
:multiple="true"
:mode="mode"
class="mb-20 mt-20"
@update:value="update"
/>
<LabeledSelect
v-if="showAdvanced"
v-model:value="value.spec.volumeMode"
:label="t('harvester.volume.volumeMode')"
:options="volumeModeOptions"
:mode="mode"
class="mb-20"
@update:value="update"
/>
</Tab>
<Tab
v-if="!isCreate"
@@ -581,6 +713,15 @@ export default {
:label="t('nameNsDescription.name.label')"
/>
</Tab>
<Tab
v-if="!isCreate"
name="conditions"
:label="t('harvester.volume.tabs.conditions')"
class="bordered-table"
:mode="mode"
>
<Conditions :value="value" />
</Tab>
</ResourceTabs>
</CruResource>
</template>
@@ -0,0 +1,196 @@
<script>
import debounce from 'lodash/debounce';
import { _EDIT, _VIEW } from '@shell/config/query-params';
import { removeAt } from '@shell/utils/array';
import { Banner } from '@components/Banner';
import InfoBox from '@shell/components/InfoBox';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
export default {
name: 'AccessControlList',
emits: ['update:value'],
components: {
Banner, InfoBox, LabeledSelect, LabeledInput
},
props: {
value: {
type: Array,
default: null,
},
mode: {
type: String,
default: _EDIT,
},
},
data() {
const rows = (this.value || []).map((row) => {
return {
action: row.action || '',
direction: row.direction || '',
priority: row.priority || 0,
match: row.match || '',
};
});
return { rows };
},
computed: {
isView() {
return this.mode === _VIEW;
},
actionOptions() {
return [
{ label: 'Allow', value: 'allow' },
{ label: 'Drop', value: 'drop' },
{ label: 'Pass', value: 'pass' },
{ label: 'Reject', value: 'reject' },
{ label: 'Allow-related', value: 'allow-related' },
{ label: 'Allow-stateless', value: 'allow-stateless' },
];
},
directionOptions() {
return [
{ label: 'To-lport', value: 'to-lport' },
{ label: 'From-lport', value: 'from-lport' },
];
}
},
created() {
this.queueUpdate = debounce(this.update, 100);
},
methods: {
add() {
this.rows.push({
action: '',
direction: '',
priority: this.rows.length,
match: '',
});
this.queueUpdate();
},
removeRule(idx) {
removeAt(this.rows, idx);
this.queueUpdate();
},
update() {
if (this.isView) {
return;
}
this.$emit('update:value', this.rows);
}
},
};
</script>
<template>
<div>
<Banner
v-if="rows.length > 0"
color="info"
>
<t
k="harvester.subnet.acl.banner"
:raw="true"
/>
</Banner>
<div
v-for="(row, idx) in rows"
:key="idx"
>
<InfoBox class="box">
<button
type="button"
class="role-link btn btn-sm removeBtn"
:disabled="isView"
@click="removeRule(idx)"
>
<i class="icon icon-x" />
</button>
<div class="row">
<div class="col span-4">
<LabeledSelect
v-model:value="row.action"
:mode="mode"
class="mt-5 ml-5"
:options="actionOptions"
:required="true"
:label="t('harvester.subnet.acl.action.label')"
:placeholder="t('harvester.subnet.acl.action.placeholder')"
@update:value="update"
/>
</div>
<div class="col span-4">
<LabeledSelect
v-model:value="row.direction"
:mode="mode"
:options="directionOptions"
class="mt-5"
:required="true"
:label="t('harvester.subnet.acl.direction.label')"
:placeholder="t('harvester.subnet.acl.direction.placeholder')"
@update:value="update"
/>
</div>
<div class="col span-3">
<LabeledInput
v-model:value.number="row.priority"
type="number"
class="mb-20 mt-5"
:max="32767"
:min="0"
:mode="mode"
required
label-key="harvester.subnet.acl.priority.label"
@update:value="update"
/>
</div>
</div>
<div class="row">
<div class="col span-11">
<LabeledInput
v-model:value="row.match"
class="mb-5 ml-5"
:mode="mode"
required
:placeholder="t('harvester.subnet.acl.match.placeholder')"
label-key="harvester.subnet.acl.match.label"
@update:value="update"
/>
</div>
</div>
</InfoBox>
</div>
<button
type="button"
class="btn role-tertiary add"
:disabled="isView"
@click="add()"
>
<t k="harvester.subnet.acl.addRule" />
</button>
</div>
</template>
<style lang="scss" scoped>
.box {
position: relative;
}
.removeBtn {
position: absolute;
top: 10px;
right: 10px;
padding: 0px;
}
</style>
+79 -3
View File
@@ -14,6 +14,8 @@ import ArrayList from '@shell/components/form/ArrayList';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../../types';
import ResourceTabs from '@shell/components/form/ResourceTabs/index';
import { Banner } from '@components/Banner';
import AccessControlList from './AccessControlList';
export default {
name: 'EditSubnet',
@@ -21,6 +23,7 @@ export default {
emits: ['update:value'],
components: {
Banner,
CruResource,
LabeledInput,
LabeledSelect,
@@ -30,6 +33,7 @@ export default {
ArrayList,
ResourceTabs,
Loading,
AccessControlList
},
mixins: [CreateEditView],
@@ -38,7 +42,9 @@ export default {
created() {
const vpc = this.$route.query.vpc || '';
const enableDHCP = this.value?.spec?.enableDHCP || false;
set(this.value.spec, 'enableDHCP', enableDHCP);
set(this.value, 'spec', this.value.spec || {
cidrBlock: '',
protocol: NETWORK_PROTOCOL.IPv4,
@@ -46,7 +52,9 @@ export default {
vpc,
gatewayIP: '',
excludeIps: [],
private: false
private: false,
enableDHCP,
acls: []
});
},
@@ -81,7 +89,6 @@ export default {
protocolOptions() {
return Object.values(NETWORK_PROTOCOL);
},
provider: {
get() {
const raw = this.value.spec.provider;
@@ -125,10 +132,21 @@ export default {
}
},
watch: {
'value.spec.enableDHCP': {
handler(newValue) {
if (newValue === false) {
this.value.spec.dhcpV4Options = '';
this.value.spec.dhcpV6Options = '';
}
},
}
},
methods: {
async saveSubnet(buttonCb) {
const errors = [];
const name = this.value?.metadata?.name;
const hasEmptyAcls = this.value?.spec?.acls?.some((acl) => !acl.match || !acl.action || acl.priority === undefined || acl.priority === null);
try {
if (!name) {
@@ -139,6 +157,8 @@ export default {
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.provider.label') }, true));
} else if (this.value.spec.excludeIps.includes('')) {
errors.push(this.t('harvester.validation.subnet.excludeIps'));
} else if (hasEmptyAcls) {
errors.push(this.t('harvester.validation.subnet.aclEmptyError'));
}
if (errors.length > 0) {
@@ -245,6 +265,45 @@ export default {
/>
</div>
</div>
<div class="row mt-20">
<div class="col span-6">
<RadioGroup
v-model:value="value.spec.enableDHCP"
name="enabled"
:options="[true, false]"
:label="t('harvester.subnet.dhcp.label')"
:labels="[t('generic.enabled'), t('generic.disabled')]"
:mode="mode"
:tooltip="t('harvester.subnet.dhcp.tooltip')"
/>
<LabeledInput
v-if="value.spec.enableDHCP && value.spec.protocol === 'IPv4'"
v-model:value="value.spec.dhcpV4Options"
class="mb-20 mt-20"
:placeholder="t('harvester.subnet.dhcp.placeholder')"
:label="t('harvester.subnet.dhcp.v4Options')"
:mode="mode"
/>
<LabeledInput
v-if="value.spec.enableDHCP && value.spec.protocol === 'IPv6'"
v-model:value="value.spec.dhcpV6Options"
:placeholder="t('harvester.subnet.dhcp.placeholder')"
class="mb-20 mt-20"
:label="t('harvester.subnet.dhcp.v6Options')"
:mode="mode"
/>
<Banner
v-if="value.spec.enableDHCP"
color="info"
class="dhcpOption-banner"
>
<t
k="harvester.subnet.dhcp.dhcpOptionBanner"
:raw="true"
/>
</Banner>
</div>
</div>
<div class="row mt-20">
<div class="col span-6">
<RadioGroup
@@ -312,12 +371,29 @@ export default {
<div class="key">
<input
v-model="scope.row.value"
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
:placeholder="t('harvester.subnet.excludeIPs.placeholder')"
/>
</div>
</template>
</ArrayList>
</Tab>
<Tab
name="ACL"
:label="t('harvester.subnet.acl.label')"
:weight="-2"
class="bordered-table"
>
<AccessControlList
v-model:value="value.spec.acls"
:mode="mode"
/>
</Tab>
</ResourceTabs>
</CruResource>
</template>
<style lang="scss" scoped>
.dhcpOption-banner {
width: max-content;
}
</style>
@@ -2,12 +2,17 @@
import debounce from 'lodash/debounce';
import { _EDIT, _VIEW } from '@shell/config/query-params';
import { removeAt } from '@shell/utils/array';
import { Banner } from '@components/Banner';
import { DOC } from '../../config/doc-links';
import { docLink } from '../../utils/feature-flags';
export default {
name: 'StaticRoutes',
emits: ['update:value'],
components: { Banner },
props: {
value: {
type: Array,
@@ -45,6 +50,12 @@ export default {
nextHopIPTooltip() {
return this.t('harvester.vpc.staticRoutes.nextHopIP.tooltip');
},
vpcPeeringExamplesLink() {
const version = this.$store.getters['harvester-common/getServerVersion']();
return docLink(DOC.VPC_CONFIGURATION_EXAMPLES, version);
},
},
created() {
@@ -78,6 +89,13 @@ export default {
<template>
<div>
<Banner color="info">
<t
k="harvester.vpc.vpcPeerings.infoBanner"
:raw="true"
:url="vpcPeeringExamplesLink"
/>
</Banner>
<div
v-if="rows.length"
class="static-route-row"
@@ -0,0 +1,133 @@
<script>
import YAML from 'yaml';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { CONFIG_MAP } from '@shell/config/types';
import { Banner } from '@components/Banner';
const CPU_MODEL_CONFIG_MAP_ID = 'harvester-system/node-cpu-model-configuration';
export default {
name: 'HarvesterCpuModel',
emits: ['update:value'],
components: {
LabeledSelect,
Banner
},
props: {
value: {
type: String,
default: ''
},
mode: {
type: String,
default: 'create',
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
try {
await this.$store.dispatch(`${ inStore }/find`, { type: CONFIG_MAP, id: CPU_MODEL_CONFIG_MAP_ID });
this.fetchError = null;
} catch (e) {
this.fetchError = this.t('harvester.virtualMachine.cpuModel.fetchError', { error: e.message || e });
}
},
data() {
return { fetchError: null };
},
computed: {
localValue: {
get() {
return this.value ?? '';
},
set(val) {
this.$emit('update:value', val ?? '');
}
},
cpuModelConfigMap() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/byId`](
CONFIG_MAP,
CPU_MODEL_CONFIG_MAP_ID
);
},
cpuModelOptions() {
if (!this.cpuModelConfigMap?.data?.cpuModels) {
return [{ label: this.t('generic.default'), value: '' }];
}
let cpuModelsData;
try {
cpuModelsData = YAML.parse(this.cpuModelConfigMap.data?.cpuModels || '');
} catch (e) {
return [{ label: this.t('generic.default'), value: '' }];
}
const options = [];
options.push({
label: this.t('generic.default'),
value: ''
});
// Add global models (host-model, host-passthrough)
const globalModels = cpuModelsData.globalModels || [];
globalModels.forEach((modelName) => {
options.push({
label: modelName,
value: modelName
});
});
// Add regular models with node count
const modelEntries = Object.entries(cpuModelsData.models || {});
// Sort models alphabetically for consistent display
modelEntries.sort((a, b) => a[0].localeCompare(b[0]));
modelEntries.forEach(([modelName, modelInfo]) => {
const readyCount = modelInfo.readyCount || 0;
const label = this.t('harvester.virtualMachine.cpuModel.optionLabel', { modelName, count: readyCount });
options.push({
label,
value: modelName
});
});
return options;
},
},
};
</script>
<template>
<div>
<Banner
v-if="fetchError"
color="error"
class="mb-20"
>
{{ fetchError }}
</Banner>
<LabeledSelect
v-model:value="localValue"
:label="t('harvester.virtualMachine.cpuModel.label')"
:options="cpuModelOptions"
:mode="mode"
:disabled="!!fetchError"
/>
</div>
</template>
@@ -1,6 +1,5 @@
<script>
import InfoBox from '@shell/components/InfoBox';
import { NETWORK_ATTACHMENT } from '@shell/config/types';
import { sortBy } from '@shell/utils/sort';
import { clone } from '@shell/utils/object';
@@ -14,6 +13,13 @@ export default {
components: { InfoBox, Base },
props: {
vm: {
type: Object,
default: () => {
return {};
}
},
mode: {
type: String,
default: 'create'
@@ -32,10 +38,15 @@ export default {
}
},
async fetch() {
await this.fetchHotunplugData();
},
data() {
return {
rows: this.addKeyId(clone(this.value)),
nameIdx: 1
nameIdx: 1,
rows: this.addKeyId(clone(this.value)),
hotunpluggableNics: new Set(),
};
},
@@ -64,15 +75,57 @@ export default {
return out;
},
canCheckHotunplug() {
return !!this.vm?.actions?.findHotunpluggableNics;
},
vmState() {
return this.vm?.stateDisplay;
}
},
watch: {
value(neu) {
this.rows = neu;
this.rows = this.mergeHotplugData(clone(neu));
},
vmState(newState, oldState) {
if (newState !== oldState) {
this.fetchHotunplugData();
}
}
},
methods: {
async fetchHotunplugData() {
if (!this.canCheckHotunplug) {
this.rows = this.mergeHotplugData(clone(this.value));
return;
}
try {
const resp = await this.vm.doAction('findHotunpluggableNics');
this.hotunpluggableNics = new Set(resp?.interfaces || []);
} catch (e) {
// eslint-disable-next-line no-console
console.error('Failed to fetch hot-unpluggable NICs:', e);
this.hotunpluggableNics = new Set();
}
this.rows = this.mergeHotplugData(clone(this.value));
},
mergeHotplugData(networks) {
return (networks || []).map((network) => ({
...network,
isHotunpluggable: this.hotunpluggableNics.has(network.name),
rowKeyId: network.rowKeyId || randomStr(10)
}));
},
add(type) {
const name = this.generateName();
@@ -118,7 +171,11 @@ export default {
update() {
this.$emit('update:value', this.rows);
}
},
unplugNIC(network) {
this.vm.unplugNIC(network.name);
},
}
};
</script>
@@ -129,17 +186,28 @@ export default {
v-for="(row, i) in rows"
:key="i"
>
<button
v-if="!isView"
type="button"
class="role-link remove-vol"
@click="remove(row)"
>
<i class="icon icon-x" />
</button>
<h3> {{ t('harvester.virtualMachine.network.title') }} </h3>
<div class="box-title mb-10">
<h3>
{{ t('harvester.virtualMachine.network.title') }}
</h3>
<button
v-if="!isView"
type="button"
class="role-link btn btn-sm remove"
@click="remove(row)"
>
<i class="icon icon-x" />
</button>
<button
v-if="vm.hotplugNicFeatureEnabled && row.isHotunpluggable && isView"
type="button"
class="role-link btn btn-sm remove"
:disabled="!canCheckHotunplug"
@click="unplugNIC(row)"
>
{{ t('harvester.virtualMachine.hotUnplug.detachNIC.actionLabel') }}
</button>
</div>
<Base
v-model:value="rows[i]"
:rows="rows"
@@ -162,16 +230,13 @@ export default {
</template>
<style lang='scss' scoped>
.infoBox{
position: relative;
}
.box-title{
display: flex;
justify-content: space-between;
align-items: center;
.remove-vol {
position: absolute;
top: 10px;
right: 16px;
padding:0px;
max-height: 28px;
min-height: 28px;
h3 {
margin-bottom: 0;
}
}
</style>
@@ -31,6 +31,7 @@ export default {
const _hash = {
pciclaims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.PCI_CLAIM }),
sriovs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOV }),
srigpuovs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOVGPU_DEVICE }),
};
await allHash(_hash);
@@ -106,19 +107,32 @@ export default {
},
computed: {
parentSriovOptions() {
allSriovs() {
const inStore = this.$store.getters['currentProduct'].inStore;
const allSriovs = this.$store.getters[`${ inStore }/all`](HCI.SR_IOV) || [];
return allSriovs.map((sriov) => {
return sriov.id;
});
return this.$store.getters[`${ inStore }/all`](HCI.SR_IOV) || [];
},
allSriovGPUs() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/all`](HCI.SR_IOVGPU_DEVICE) || [];
},
parentSriovOptions() {
return this.allSriovs.map((sriov) => sriov.id);
},
parentSriovGPUOptions() {
return this.allSriovGPUs.map((sriovgpu) => sriovgpu.id);
},
parentSriovLabel() {
return HCI_ANNOTATIONS.PARENT_SRIOV;
}
},
parentSriovGPULabel() {
return HCI_ANNOTATIONS.PARENT_SRIOV_GPU;
},
vGPUAsPCIDeviceEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
},
},
methods: {
enableGroup(rows = []) {
const row = rows[0];
@@ -206,6 +220,15 @@ export default {
:rows="rows"
@change-rows="changeRows"
/>
<FilterBySriov
v-if="vGPUAsPCIDeviceEnabled"
ref="filterByParentSRIOVGPU"
:parent-sriov-options="parentSriovGPUOptions"
:parent-sriov-label="parentSriovGPULabel"
:label="t('harvester.sriov.parentSriovGPU')"
:rows="rows"
@change-rows="changeRows"
/>
</template>
</ResourceTable>
</template>
@@ -8,6 +8,7 @@ import { set } from '@shell/utils/object';
import { HCI } from '../../../types';
import DeviceList from './DeviceList';
import CompatibilityMatrix from '../CompatibilityMatrix';
import MessageLink from '@shell/components/MessageLink';
export default {
name: 'VirtualMachinePCIDevices',
@@ -15,7 +16,8 @@ export default {
LabeledSelect,
DeviceList,
CompatibilityMatrix,
Banner
Banner,
MessageLink
},
props: {
mode: {
@@ -54,6 +56,11 @@ export default {
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];
@@ -133,6 +140,13 @@ export default {
return inUse;
},
toVGpuDevicesPage() {
return {
name: 'harvester-c-cluster-resource',
params: { cluster: this.$store.getters['clusterId'], resource: HCI.VGPU_DEVICE },
};
},
devicesByNode() {
return this.enabledDevices?.reduce((acc, device) => {
const nodeName = device.status?.nodeName;
@@ -227,7 +241,12 @@ export default {
<div class="row">
<div class="col span-12">
<Banner color="info">
<t k="harvester.pci.howToUseDevice" />
<MessageLink
:to="toVGpuDevicesPage"
prefix-label="harvester.pci.howToUseDevice.prefix"
middle-label="harvester.pci.howToUseDevice.middle"
suffix-label="harvester.pci.howToUseDevice.suffix"
/>
</Banner>
<Banner
v-if="selectedDevices.length > 0"
@@ -48,6 +48,13 @@ export default {
this[key] = res[key];
}
const vmDevices = this.value?.domain?.devices?.hostDevices || [];
const vmDeviceNames = vmDevices.map(({ name }) => name);
this.devices.forEach((row) => {
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
});
this.selectedDevices = (this.value?.domain?.devices?.hostDevices || [])
.map(({ name }) => name)
.filter((name) => this.enabledDevices.find((device) => device?.metadata?.name === name));
@@ -46,6 +46,13 @@ export default {
this[key] = res[key];
}
const vmDevices = this.value?.domain?.devices?.gpus || [];
const vmDeviceNames = vmDevices.map(({ name }) => name);
this.devices.forEach((row) => {
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
});
const vGpus = this.vm.isOff ? [
...(this.value?.domain?.devices?.gpus || []).map(({ name }) => name),
] : [
@@ -13,11 +13,12 @@ import { ucFirst, randomStr } from '@shell/utils/string';
import { removeObject } from '@shell/utils/array';
import { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
import { SOURCE_TYPE } from '../../../config/harvester-map';
import { VOLUME_HOTPLUG_ACTION, SOURCE_TYPE } from '../../../config/harvester-map';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
import { HCI } from '../../../types';
import { VOLUME_MODE } from '@pkg/harvester/config/types';
import { OFF } from '../../../models/kubevirt.io.virtualmachine';
import { EMPTY_IMAGE } from '../../../utils/vm';
export default {
emits: ['update:value'],
@@ -117,6 +118,10 @@ export default {
return this.mode === _CREATE;
},
isHotplugCdRomFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('hotplugCdRom');
},
defaultStorageClass() {
const defaultStorage = this.$store.getters['harvester/all'](STORAGE_CLASS).find((sc) => sc.isDefault);
@@ -136,13 +141,17 @@ export default {
pvcs() {
return this.$store.getters['harvester/all'](PVC) || [];
},
isLHV2VolExpansionFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
},
},
watch: {
value: {
handler(neu) {
const rows = clone(neu).map((V) => {
if (!this.isCreate && V.source !== SOURCE_TYPE.CONTAINER && !V.newCreateId) {
if (!this.isCreate && V.source !== SOURCE_TYPE.CONTAINER && !V.newCreateId && V.image !== EMPTY_IMAGE) {
V.to = {
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-namespace-id`,
params: {
@@ -213,8 +222,48 @@ export default {
}
},
unplugVolume(volume) {
this.vm.unplugVolume(volume.name);
canDoVolumeHotplugAction(volume) {
if (!this.isHotplugCdRomFeatureEnabled && volume.type === 'cd-rom') {
return false;
}
if (volume.hotpluggable) {
return true;
}
return volume.type === 'cd-rom' && volume.bus === 'sata' && volume.image === EMPTY_IMAGE;
},
getVolumeHotplugAction(volume) {
if (volume.type === 'cd-rom' && volume.bus === 'sata') {
if (volume.image === EMPTY_IMAGE) {
return VOLUME_HOTPLUG_ACTION.INSERT_CDROM_IMAGE;
}
return VOLUME_HOTPLUG_ACTION.EJECT_CDROM_IMAGE;
}
return VOLUME_HOTPLUG_ACTION.DETACH_DISK;
},
getVolumeHotplugActionLabel(volume) {
const labels = {
[VOLUME_HOTPLUG_ACTION.DETACH_DISK]: 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabel',
[VOLUME_HOTPLUG_ACTION.INSERT_CDROM_IMAGE]: 'harvester.modal.insertCdRomVolume.actionLabel',
[VOLUME_HOTPLUG_ACTION.EJECT_CDROM_IMAGE]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.actionLabel',
};
return labels[this.getVolumeHotplugAction(volume)];
},
hotplugVolume(volume) {
const calls = {
[VOLUME_HOTPLUG_ACTION.DETACH_DISK]: () => this.vm.unplugVolume(volume.name),
[VOLUME_HOTPLUG_ACTION.INSERT_CDROM_IMAGE]: () => this.vm.insertCdRomVolume(volume.name),
[VOLUME_HOTPLUG_ACTION.EJECT_CDROM_IMAGE]: () => this.vm.ejectCdRomVolume(volume.name),
};
return calls[this.getVolumeHotplugAction(volume)]();
},
componentFor(type) {
@@ -270,8 +319,8 @@ export default {
},
isResizeDisabled(volume) {
if (this.isCreate) return false;
if (volume.newCreateId) return false;
if (this.isLHV2VolExpansionFeatureEnabled) return false;
if (this.isCreate || volume.newCreateId) return false;
const isStopped = this.vm.stateDisplay === OFF;
const isLonghornV2 = this.isLonghornV2(volume);
@@ -299,55 +348,57 @@ export default {
v-for="(volume, i) in rows"
:key="volume.id"
>
<InfoBox class="box">
<button
v-if="!isView"
type="button"
class="role-link btn btn-sm remove"
@click="removeVolume(volume)"
>
<i class="icon icon-x" />
</button>
<button
v-if="volume.hotpluggable && isView"
type="button"
class="role-link btn remove"
@click="unplugVolume(volume)"
>
{{ t('harvester.virtualMachine.unplug.detachVolume') }}
</button>
<h3>
<span
v-if="volume.to && isVirtualType"
class="title"
>
<router-link :to="volume.to">
{{ t('harvester.virtualMachine.volume.edit') }} {{ headerFor(volume.source) }}
</router-link>
<BadgeStateFormatter
v-if="volume.pvc"
class="ml-10 state"
:arbitrary="true"
:row="volume.pvc"
:value="volume.pvc.state"
/>
<a
v-if="dev && !!volume.pvc && !!volume.pvc.resourceExternalLink"
v-clean-tooltip="t(volume.pvc.resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
class="ml-5 resource-external"
rel="nofollow noopener noreferrer"
target="_blank"
:href="volume.pvc.resourceExternalLink.url"
<InfoBox>
<div class="box-title mb-10">
<h3>
<span
v-if="volume.to && isVirtualType"
class="title"
>
<i class="icon icon-external-link" />
</a>
</span>
<router-link :to="volume.to">
{{ t('harvester.virtualMachine.volume.edit') }} {{ headerFor(volume.source) }}
</router-link>
<span v-else>
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
</span>
</h3>
<BadgeStateFormatter
v-if="volume.pvc"
class="ml-10 state"
:arbitrary="true"
:row="volume.pvc"
:value="volume.pvc.state"
/>
<a
v-if="dev && !!volume.pvc && !!volume.pvc.resourceExternalLink"
v-clean-tooltip="t(volume.pvc.resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
class="ml-5 resource-external"
rel="nofollow noopener noreferrer"
target="_blank"
:href="volume.pvc.resourceExternalLink.url"
>
<i class="icon icon-external-link" />
</a>
</span>
<span v-else>
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
</span>
</h3>
<button
v-if="!isView"
type="button"
class="role-link btn btn-sm remove"
@click="removeVolume(volume)"
>
<i class="icon icon-x" />
</button>
<button
v-if="canDoVolumeHotplugAction(volume) && isView"
type="button"
class="role-link btn btn-sm remove"
@click="hotplugVolume(volume)"
>
{{ t(getVolumeHotplugActionLabel(volume)) }}
</button>
</div>
<div>
<component
:is="componentFor(volume.source)"
@@ -406,7 +457,7 @@ export default {
:label="ucFirst(value.volumeBackups.error.message)"
/>
<Banner
v-if="isLonghornV2(volume) && !isView"
v-if="!isLHV2VolExpansionFeatureEnabled && isLonghornV2(volume) && !isView"
color="warning"
:label="t('harvester.volume.longhorn.disableResize')"
/>
@@ -491,25 +542,24 @@ export default {
</template>
<style lang='scss' scoped>
.box {
position: relative;
.box-title {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
margin-bottom: 0;
}
}
.title {
display: flex;
align-items: center;
.state {
font-size: 16px;
}
}
.remove {
position: absolute;
top: 10px;
right: 10px;
padding: 0px;
}
.bootOrder {
display: flex;
align-items: center;
@@ -99,7 +99,6 @@ export default {
this.allPVCs
.filter( (pvc) => {
let isAvailable = true;
let isBeingUsed = false;
this.rows.forEach( (O) => {
if (O.volumeName === pvc.metadata.name) {
@@ -111,17 +110,16 @@ export default {
return false;
}
// already used as image volume
if (this.idx > 0 && pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
return false;
}
if (pvc.isGoldenImageVolume) {
return false;
}
if (pvc.attachVM && isAvailable && pvc.attachVM?.id === this.vm?.id && this.isEdit) {
isBeingUsed = false;
} else if (pvc.attachVM) {
isBeingUsed = true;
}
return isAvailable && !isBeingUsed && pvc.isAvailable;
return isAvailable && pvc.isAvailable;
})
.map((pvc) => {
return {
@@ -14,6 +14,7 @@ import { _VIEW } from '@shell/config/query-params';
import LabelValue from '@shell/components/LabelValue';
import { ucFirst } from '@shell/utils/string';
import { GIBIBYTE } from '../../../../utils/unit';
import { EMPTY_IMAGE } from '../../../../utils/vm';
export default {
name: 'HarvesterEditVMImage',
@@ -96,8 +97,20 @@ export default {
return this.mode === _VIEW;
},
isExistingCdrom() {
return this.value.type === 'cd-rom' && !this.value.newCreateId;
},
isEmptyImage() {
return this.value.image === EMPTY_IMAGE;
},
isHotplugCdRomFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('hotplugCdRom');
},
imagesOption() {
return this.images
const images = this.images
.filter((image) => {
if (!image.isReady) return false;
@@ -114,6 +127,19 @@ export default {
value: image.id,
disabled: image.isImportedImage
}));
const options = [];
if (this.isHotplugCdRomFeatureEnabled) {
options.push({
label: this.t('harvester.virtualMachine.volume.emptyImage'),
value: EMPTY_IMAGE,
disabled: false
});
}
options.push(...images);
return options;
},
imageName() {
@@ -179,6 +205,7 @@ export default {
'value.type'(neu) {
if (neu === 'cd-rom') {
this.value['bus'] = 'sata';
this.updateHotpluggable();
this.update();
}
},
@@ -221,12 +248,48 @@ export default {
return label;
},
update() {
this.value.hasDiskError = this.showDiskTooSmallError;
this.$emit('update');
},
updateHotpluggable() {
if (this.value.type !== 'cd-rom') {
this.value['hotpluggable'] = false;
} else {
this.value['hotpluggable'] = (this.value.bus === 'sata');
}
},
onTypeChange() {
if (this.value.image === EMPTY_IMAGE && this.value.type !== 'cd-rom') {
this.value['image'] = '';
}
this.updateHotpluggable();
this.update();
},
onBusChange() {
if (this.value.image === EMPTY_IMAGE && this.value.bus !== 'sata') {
this.value['image'] = '';
}
this.updateHotpluggable();
this.update();
},
onImageChange() {
if (this.value.image === EMPTY_IMAGE) {
this.value['type'] = 'cd-rom';
this.value['bus'] = 'sata';
this.value['size'] = `0${ GIBIBYTE }`;
this.update();
return;
}
const imageResource = this.$store.getters['harvester/all'](HCI.IMAGE)?.find( (I) => this.value.image === I.id);
const isIsoImage = /iso$/i.test(imageResource?.imageSuffix);
const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize);
@@ -239,6 +302,8 @@ export default {
this.value['bus'] = 'virtio';
}
this.updateHotpluggable();
if (imageSize) {
let imageSizeGiB = Math.ceil(imageSize / 1024 / 1024 / 1024);
@@ -256,6 +321,10 @@ export default {
},
checkImageExists(imageId) {
if (imageId === EMPTY_IMAGE) {
return;
}
if (!!imageId && this.imagesOption.length > 0 && !findBy(this.imagesOption, 'value', imageId)) {
this.$store.dispatch('growl/error', {
title: this.$store.getters['i18n/t']('harvester.vmTemplate.tips.notExistImage.title', { name: imageId }),
@@ -283,6 +352,7 @@ export default {
>
<LabeledInput
v-model:value="value.name"
:disabled="!isCreate && isExistingCdrom"
:label="t('harvester.fields.name')"
required
:mode="mode"
@@ -302,10 +372,11 @@ export default {
>
<LabeledSelect
v-model:value="value.type"
:disabled="!isCreate && isExistingCdrom"
:label="t('harvester.fields.type')"
:options="VOLUME_TYPE"
:mode="mode"
@update:value="update"
@update:value="onTypeChange"
/>
</InputOrDisplay>
</div>
@@ -323,7 +394,7 @@ export default {
>
<LabeledSelect
v-model:value="value.image"
:disabled="idx === 0 && !isCreate && !value.newCreateId && isVirtualType"
:disabled="(idx === 0 || isExistingCdrom) && (!isCreate && !value.newCreateId && isVirtualType)"
:label="t('harvester.fields.image')"
:options="imagesOption"
:mode="mode"
@@ -351,7 +422,7 @@ export default {
:label="t('harvester.fields.size')"
:mode="mode"
:required="validateRequired"
:disabled="isResizeDisabled"
:disabled="isResizeDisabled || isEmptyImage || (!isCreate && isExistingCdrom)"
:suffix="GIBIBYTE"
@update:value="update"
/>
@@ -374,7 +445,8 @@ export default {
:label="t('harvester.virtualMachine.volume.bus')"
:mode="mode"
:options="InterfaceOption"
@update:value="update"
:disabled="!isCreate && isExistingCdrom"
@update:value="onBusChange"
/>
</InputOrDisplay>
</div>
@@ -32,6 +32,7 @@ import PciDevices from './VirtualMachinePciDevices/index';
import AccessCredentials from './VirtualMachineAccessCredentials';
import CloudConfig from './VirtualMachineCloudConfig';
import CpuMemory from './VirtualMachineCpuMemory';
import CpuModel from './VirtualMachineCpuModel';
import Network from './VirtualMachineNetwork';
import Volume from './VirtualMachineVolume';
import SSHKey from './VirtualMachineSSHKey';
@@ -57,6 +58,7 @@ export default {
SSHKey,
Network,
CpuMemory,
CpuModel,
CloudConfig,
NodeScheduling,
PodAffinity,
@@ -96,7 +98,6 @@ export default {
templateVersionId: '',
namePrefix: '',
isSingle: true,
useTemplate: false,
isOpen: false,
hostname,
isRestartImmediately,
@@ -210,6 +211,10 @@ export default {
return false;
},
vGPUAsPCIDeviceEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
},
usbPassthroughEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
},
@@ -255,6 +260,7 @@ export default {
return volume;
});
delete cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.MAC_ADDRESS];
cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
// Update instance labels, labels and annotations
@@ -434,38 +440,44 @@ export default {
}
},
_compareDisksIgnoreStorage(disksA = [], disksB = []) {
if (disksA.length !== disksB.length) return false;
const sortedA = [...disksA].sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
const sortedB = [...disksB].sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
for (let i = 0; i < sortedA.length; i++) {
const cloneA = clone(sortedA[i]);
const cloneB = clone(sortedB[i]);
// remove the storage field before comparison
delete cloneA?.spec?.resources?.requests?.storage;
delete cloneB?.spec?.resources?.requests?.storage;
if (!isEqual(cloneA, cloneB)) return false;
}
return true;
},
_shouldPromptRestart(oldVM, newVM) {
const specChanged = !isEqual(oldVM?.spec, newVM?.spec);
// check if the disk defined in annotations has changed, ignoring storage size
const oldDisks = parseVolumeClaimTemplates(oldVM);
const newDisks = parseVolumeClaimTemplates(newVM);
const diskChanged = !this._compareDisksIgnoreStorage(oldDisks, newDisks);
return specChanged || diskChanged;
},
restartVM() {
if (this.mode !== 'edit') {
return;
}
if (!this.value.isRunning) {
return;
}
const cloneDeepNewVM = clone(this.value);
if (this.mode !== 'edit' || !this.value.isRunning) return;
// new VM
delete cloneDeepNewVM?.metadata;
delete cloneDeepNewVM?.__clone;
const needsRestart = this._shouldPromptRestart(this.cloneVM, this.value);
// old VM
delete this.cloneVM?.metadata;
delete this.cloneVM?.__clone;
// add empty hostDevices to old VM as CRD does not have it.
const devicesObj = this.cloneVM?.spec?.template?.spec?.domain?.devices;
if (devicesObj && devicesObj.hostDevices === undefined) {
set(devicesObj, 'hostDevices', []);
}
const oldVM = JSON.parse(JSON.stringify(this.cloneVM));
const newVM = JSON.parse(JSON.stringify(cloneDeepNewVM));
// we won't show restart dialog in yaml page as we don't have a way to detect change in yaml editor.
// only check VM is changed in form page.
if (isEqual(oldVM, newVM)) {
return;
}
// if no restart is needed (either no changes or only size changed)
if (!needsRestart) return;
return new Promise((resolve) => {
this.isOpen = true;
@@ -484,6 +496,7 @@ export default {
if (this.isSingle) {
if (!this.value.spec.template.spec.hostname) {
this.value.spec.template.spec['hostname'] = this.value.metadata.name;
this.spec.template.spec['hostname'] = this.value.metadata.name;
}
}
@@ -531,6 +544,18 @@ export default {
return out;
},
updateCpuModel(value) {
if (!this.spec?.template?.spec?.domain?.cpu) {
set(this.spec, 'template.spec.domain.cpu', {});
}
if (value && value !== '') {
set(this.spec.template.spec.domain.cpu, 'model', value);
} else {
delete this.spec.template.spec.domain.cpu.model;
}
},
},
};
</script>
@@ -719,7 +744,7 @@ export default {
</Tab>
<Tab
v-if="enabledSriovgpu"
v-if="enabledSriovgpu && !vGPUAsPCIDeviceEnabled"
:label="t('harvester.tab.vGpuDevices')"
name="vGpuDevices"
:weight="-6"
@@ -863,6 +888,16 @@ export default {
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<CpuModel
v-model:value="cpuModel"
:mode="mode"
@update:value="updateCpuModel"
/>
</div>
</div>
<div class="row mb-20">
<a
v-if="showAdvanced"
@@ -892,7 +927,10 @@ export default {
/>
</div>
<div class="col span-6">
<div
v-if="!value.vmMachineTypeAutoFeatureEnabled"
class="col span-6"
>
<LabeledSelect
v-model:value="machineType"
label-key="harvester.virtualMachine.input.MachineType"
@@ -39,6 +39,10 @@ export default {
return this.mode === _VIEW;
},
disableAdd() {
return this.isStandaloneHarvester && this.rows.some((row) => row.namespace === '*');
},
showAdd() {
return !this.isView;
},
@@ -141,6 +145,7 @@ export default {
>
<button
type="button"
:disabled="disableAdd"
class="btn role-tertiary add"
@click="add()"
>
@@ -66,17 +66,26 @@ export default {
},
namespaceOptions() {
const out = (this.filteredNamespaces || []).map((namespace) => {
return {
label: namespace.metadata.name,
value: namespace.id,
};
});
const namespaces = this.filteredNamespaces;
const selected = this.rows.map((r) => r?.namespace);
if (this.isStandaloneHarvester) {
return [
{ label: this.t('generic.all'), value: '*' },
...namespaces.map((ns) => ({
label: ns.metadata.name,
value: ns.id,
disabled: selected.includes(ns.id) && ns.id !== this.row.namespace
}))
];
}
return [{
label: this.t('generic.all'),
value: '*',
}, ...out];
},
...namespaces.map((ns) => ({ label: ns.metadata.name, value: ns.id }))
];
},
guestClusterOptions() {
@@ -0,0 +1,357 @@
<script>
import CruResource from '@shell/components/CruResource';
import Tabbed from '@shell/components/Tabbed';
import Tab from '@shell/components/Tabbed/Tab';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import { RadioGroup } from '@components/Form/Radio';
import UnitInput from '@shell/components/form/UnitInput';
import CreateEditView from '@shell/mixins/create-edit-view';
import FormValidation from '@shell/mixins/form-validation';
import { SECRET } from '@shell/config/types';
import { randomStr } from '@shell/utils/string';
import { mapGetters } from 'vuex';
export default {
name: 'EditOpenstackSource',
emits: ['update:value'],
components: {
CruResource,
Tabbed,
Tab,
LabeledInput,
LabeledSelect,
NameNsDescription,
RadioGroup,
UnitInput
},
mixins: [CreateEditView, FormValidation],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
},
mode: {
type: String,
required: true,
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.allSecrets = await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
},
data() {
if (!this.value.spec) this.value.spec = {};
if (!this.value.spec.credentials) this.value.spec.credentials = {};
const initialMode = this.value.spec.credentials.name ? 'existing' : 'new';
return {
allSecrets: [],
authMode: initialMode,
newUsername: '',
newPassword: '',
newProjectName: '',
newDomainName: '',
newCaCert: '',
// Rules for fields that exist in the value object (Model)
fvFormRuleSets: [
{ path: 'metadata.name', rules: ['nameRequired'] },
{ path: 'spec.endpoint', rules: ['endpointRequired'] },
{ path: 'spec.region', rules: ['regionRequired'] },
],
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
authModeOptions() {
return [
{ label: this.t('harvester.addons.vmImport.fields.createSecret'), value: 'new' },
{ label: this.t('harvester.addons.vmImport.fields.useSecret'), value: 'existing' }
];
},
secretOptions() {
const currentNamespace = this.value.metadata.namespace || 'default';
return this.allSecrets
.filter((s) => s.metadata.namespace === currentNamespace)
.map((s) => ({
label: s.nameDisplay,
value: s.metadata.name
}));
},
// Define custom rules for the FormValidation mixin
fvExtraRules() {
return {
nameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.fields.name') }) : undefined,
endpointRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.openstack.fields.endpoint') }) : undefined,
regionRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.openstack.fields.region') }) : undefined,
};
},
// Combine mixin validation + conditional manual checks
isFormValid() {
// Check static fields via Mixin
if (!this.fvFormIsValid) {
return false;
}
// Check conditional fields
if (this.authMode === 'new') {
if (!this.newUsername || !this.newPassword) return false;
if (!this.newProjectName || !this.newDomainName) return false;
} else {
if (!this.value.spec.credentials.name) return false;
}
return true;
}
},
methods: {
usernameRule(val) {
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.username') }) : undefined;
},
passwordRule(val) {
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.password') }) : undefined;
},
projectRule(val) {
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.openstack.fields.projectName') }) : undefined;
},
domainRule(val) {
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.openstack.fields.domainName') }) : undefined;
},
secretRule(val) {
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.selectSecret') }) : undefined;
},
async saveSource(buttonCb) {
const inStore = this.$store.getters['currentProduct'].inStore;
try {
if (this.authMode === 'new') {
const secretName = `${ this.value.metadata.name }-creds-${ randomStr(4).toLowerCase() }`;
const namespace = this.value.metadata.namespace || 'default';
const newSecret = await this.$store.dispatch(`${ inStore }/create`, {
type: SECRET,
metadata: {
name: secretName,
namespace
}
});
newSecret['_type'] = 'Opaque';
newSecret['data'] = {
username: btoa(this.newUsername),
password: btoa(this.newPassword),
project_name: btoa(this.newProjectName),
domain_name: btoa(this.newDomainName),
ca_cert: this.newCaCert ? btoa(this.newCaCert) : undefined
};
await newSecret.save();
this.value.spec.credentials = {
name: secretName,
namespace
};
}
await this.save(buttonCb);
} catch (err) {
this.errors = [err];
buttonCb(false);
}
}
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
:validation-passed="isFormValid"
@finish="saveSource"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
:mode="mode"
:rules="{ name: fvGetAndReportPathRules('metadata.name') }"
@update:value="$emit('update:value', $event)"
/>
<Tabbed
v-bind="$attrs"
class="mt-15"
:side-tabs="true"
>
<Tab
name="basic"
:label="t('harvester.addons.vmImport.titles.basic')"
:weight="3"
>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model:value="value.spec.endpoint"
:label="t('harvester.addons.vmImport.openstack.fields.endpoint')"
:placeholder="t('harvester.addons.vmImport.openstack.placeholders.endpoint')"
:mode="mode"
:rules="fvGetAndReportPathRules('spec.endpoint')"
required
/>
</div>
<div class="col span-6">
<LabeledInput
v-model:value="value.spec.region"
:label="t('harvester.addons.vmImport.openstack.fields.region')"
:placeholder="t('harvester.addons.vmImport.openstack.placeholders.region')"
:mode="mode"
:rules="fvGetAndReportPathRules('spec.region')"
required
/>
</div>
</div>
</Tab>
<Tab
name="auth"
:label="t('harvester.addons.vmImport.titles.auth')"
:weight="2"
>
<div class="row mb-20">
<div class="col span-12">
<RadioGroup
v-model:value="authMode"
name="authMode"
:options="authModeOptions"
:mode="mode"
/>
</div>
</div>
<div v-if="authMode === 'new'">
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model:value="newUsername"
:label="t('harvester.addons.vmImport.fields.username')"
:mode="mode"
:rules="[usernameRule]"
required
/>
</div>
<div class="col span-6">
<LabeledInput
v-model:value="newPassword"
type="password"
:label="t('harvester.addons.vmImport.fields.password')"
:mode="mode"
:rules="[passwordRule]"
required
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model:value="newProjectName"
:label="t('harvester.addons.vmImport.openstack.fields.projectName')"
:placeholder="t('harvester.addons.vmImport.openstack.placeholders.projectName')"
:mode="mode"
:rules="[projectRule]"
required
/>
</div>
<div class="col span-6">
<LabeledInput
v-model:value="newDomainName"
:label="t('harvester.addons.vmImport.openstack.fields.domainName')"
:placeholder="t('harvester.addons.vmImport.openstack.placeholders.domainName')"
:mode="mode"
:rules="[domainRule]"
required
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-12">
<LabeledInput
v-model:value="newCaCert"
type="multiline"
:label="t('harvester.addons.vmImport.fields.caCert')"
:placeholder="t('harvester.addons.vmImport.placeholders.caCert')"
:min-height="100"
:mode="mode"
/>
</div>
</div>
</div>
<div v-if="authMode === 'existing'">
<div class="row mb-20">
<div class="col span-6">
<LabeledSelect
v-model:value="value.spec.credentials.name"
:options="secretOptions"
:label="t('harvester.addons.vmImport.fields.selectSecret')"
:mode="mode"
:rules="[secretRule]"
required
/>
</div>
</div>
</div>
</Tab>
<Tab
name="advanced"
:label="t('harvester.addons.vmImport.titles.advanced')"
:weight="1"
>
<div class="row mb-20">
<div class="col span-6">
<UnitInput
v-model:value="value.spec.uploadImageRetryCount"
:label="t('harvester.addons.vmImport.openstack.fields.retryCount')"
:placeholder="t('harvester.addons.vmImport.openstack.placeholders.retryCount')"
suffix="Times"
:mode="mode"
/>
</div>
<div class="col span-6">
<UnitInput
v-model:value="value.spec.uploadImageRetryDelay"
:label="t('harvester.addons.vmImport.openstack.fields.retryDelay')"
:placeholder="t('harvester.addons.vmImport.openstack.placeholders.retryDelay')"
suffix="Seconds"
:mode="mode"
/>
</div>
</div>
</Tab>
</Tabbed>
</CruResource>
</template>
@@ -0,0 +1,322 @@
<script>
import CruResource from '@shell/components/CruResource';
import Tabbed from '@shell/components/Tabbed';
import Tab from '@shell/components/Tabbed/Tab';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import { RadioGroup } from '@components/Form/Radio';
import UnitInput from '@shell/components/form/UnitInput';
import CreateEditView from '@shell/mixins/create-edit-view';
import FormValidation from '@shell/mixins/form-validation';
import { SECRET } from '@shell/config/types';
import { randomStr } from '@shell/utils/string';
import { mapGetters } from 'vuex';
export default {
name: 'EditOvaSource',
emits: ['update:value'],
components: {
CruResource,
Tabbed,
Tab,
LabeledInput,
LabeledSelect,
NameNsDescription,
RadioGroup,
UnitInput
},
mixins: [CreateEditView, FormValidation],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
},
mode: {
type: String,
required: true,
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.allSecrets = await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
},
data() {
if (!this.value.spec) this.value.spec = {};
// Auth is optional for OVA (public URLs).
// If credentials.name exists -> Existing.
// If not -> None (default).
let initialMode = 'none';
if (this.value.spec.credentials?.name) {
initialMode = 'existing';
}
return {
allSecrets: [],
authMode: initialMode,
newUsername: '',
newPassword: '',
newCaCert: '', // Key will be "ca.crt"
// Validation Rules for static fields
fvFormRuleSets: [
{ path: 'metadata.name', rules: ['nameRequired'] },
{ path: 'spec.url', rules: ['urlRequired'] },
],
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
authModeOptions() {
return [
{ label: this.t('harvester.addons.vmImport.fields.none'), value: 'none' },
{ label: this.t('harvester.addons.vmImport.fields.createSecret'), value: 'new' },
{ label: this.t('harvester.addons.vmImport.fields.useSecret'), value: 'existing' }
];
},
secretOptions() {
const currentNamespace = this.value.metadata.namespace || 'default';
return this.allSecrets
.filter((s) => s.metadata.namespace === currentNamespace)
.map((s) => ({
label: s.nameDisplay,
value: s.metadata.name
}));
},
// Define custom rules for the FormValidation mixin
fvExtraRules() {
return {
nameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.fields.name') }) : undefined,
urlRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.ova.fields.url') }) : undefined,
};
},
isFormValid() {
if (!this.fvFormIsValid) {
return false;
}
if (this.authMode === 'new') {
// At least a username/password OR a CA cert to be provided.
// If the user selected "Create New", they likely intend to enter something.
if (!this.newUsername && !this.newPassword && !this.newCaCert) return false;
} else if (this.authMode === 'existing') {
if (!this.value.spec.credentials?.name) return false;
}
return true;
}
},
watch: {
authMode(newMode) {
if (newMode === 'existing') {
// Bind to value.spec.credentials.name for existing credential
// Ensure 'credentials' object exists first when selected
if (!this.value.spec.credentials) {
this.value.spec.credentials = {
name: '',
namespace: this.value.metadata.namespace || 'default'
};
}
}
}
},
methods: {
secretRule(val) {
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.selectSecret') }) : undefined;
},
async saveSource(buttonCb) {
const inStore = this.$store.getters['currentProduct'].inStore;
try {
if (this.authMode === 'none') {
// Clear any credential reference
delete this.value.spec.credentials;
} else if (this.authMode === 'new') {
const secretName = `${ this.value.metadata.name }-creds-${ randomStr(4).toLowerCase() }`;
const namespace = this.value.metadata.namespace || 'default';
const newSecret = await this.$store.dispatch(`${ inStore }/create`, {
type: SECRET,
metadata: {
name: secretName,
namespace
}
});
newSecret['_type'] = 'Opaque';
newSecret['data'] = {
// Optional fields logic
username: this.newUsername ? btoa(this.newUsername) : undefined,
password: this.newPassword ? btoa(this.newPassword) : undefined,
// vm-import-controller code specifies "ca.crt" with a dot.
'ca.crt': this.newCaCert ? btoa(this.newCaCert) : undefined
};
await newSecret.save();
this.value.spec.credentials = {
name: secretName,
namespace
};
}
await this.save(buttonCb);
} catch (err) {
this.errors = [err];
buttonCb(false);
}
}
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
:validation-passed="isFormValid"
@finish="saveSource"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
:mode="mode"
:rules="{ name: fvGetAndReportPathRules('metadata.name') }"
@update:value="$emit('update:value', $event)"
/>
<Tabbed
v-bind="$attrs"
class="mt-15"
:side-tabs="true"
>
<Tab
name="basic"
:label="t('harvester.addons.vmImport.titles.basic')"
:weight="3"
>
<div class="row mb-20">
<div class="col span-12">
<LabeledInput
v-model:value="value.spec.url"
:label="t('harvester.addons.vmImport.ova.fields.url')"
:placeholder="t('harvester.addons.vmImport.ova.placeholders.url')"
tooltip="Supports HTTP and HTTPS protocols."
:mode="mode"
:rules="fvGetAndReportPathRules('spec.url')"
required
/>
</div>
</div>
</Tab>
<Tab
name="auth"
:label="t('harvester.addons.vmImport.titles.auth')"
:weight="2"
>
<div class="row mb-20">
<div class="col span-12">
<RadioGroup
v-model:value="authMode"
name="authMode"
:options="authModeOptions"
:mode="mode"
/>
</div>
</div>
<div v-if="authMode === 'new'">
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model:value="newUsername"
:label="t('harvester.addons.vmImport.fields.username')"
placeholder="(Optional)"
:mode="mode"
/>
</div>
<div class="col span-6">
<LabeledInput
v-model:value="newPassword"
type="password"
:label="t('harvester.addons.vmImport.fields.password')"
placeholder="(Optional)"
:mode="mode"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-12">
<LabeledInput
v-model:value="newCaCert"
type="multiline"
:label="t('harvester.addons.vmImport.fields.caCert')"
:placeholder="t('harvester.addons.vmImport.placeholders.caCert')"
:min-height="100"
:mode="mode"
/>
</div>
</div>
</div>
<div v-if="authMode === 'existing'">
<div class="row mb-20">
<div class="col span-6">
<LabeledSelect
v-model:value="value.spec.credentials.name"
:options="secretOptions"
:label="t('harvester.addons.vmImport.fields.selectSecret')"
:mode="mode"
:rules="[secretRule]"
required
/>
</div>
</div>
</div>
</Tab>
<Tab
name="advanced"
:label="t('harvester.addons.vmImport.titles.advanced')"
:weight="1"
>
<div class="row mb-20">
<div class="col span-12">
<UnitInput
v-model:value="value.spec.httpTimeoutSeconds"
:label="t('harvester.addons.vmImport.ova.fields.httpTimeout')"
:placeholder="t('harvester.addons.vmImport.ova.placeholders.httpTimeout')"
suffix="Seconds"
:mode="mode"
/>
</div>
</div>
</Tab>
</Tabbed>
</CruResource>
</template>
@@ -0,0 +1,570 @@
<script>
import CruResource from '@shell/components/CruResource';
import Tabbed from '@shell/components/Tabbed';
import Tab from '@shell/components/Tabbed/Tab';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import { Checkbox } from '@components/Form/Checkbox';
import CreateEditView from '@shell/mixins/create-edit-view';
import FormValidation from '@shell/mixins/form-validation';
import { STORAGE_CLASS, NETWORK_ATTACHMENT } from '@shell/config/types';
import { allHash } from '@shell/utils/promise';
import { MANAGEMENT_NETWORK } from '../mixins/harvester-vm';
import { VMIMPORT_SOURCE_PROVIDER, VMIMPORT_SOURCE_KINDS } from '../config/types';
import { HCI } from '../types';
import { isValidDNSLabelName } from '@pkg/utils/regular';
import { mapGetters } from 'vuex';
// Full API types for the fetch dispatch
const VMWARE_SOURCE_TYPE = `${ HCI.MIGRATION }.${ VMIMPORT_SOURCE_KINDS.VMWARE.toLowerCase() }`;
const OPENSTACK_SOURCE_TYPE = `${ HCI.MIGRATION }.${ VMIMPORT_SOURCE_KINDS.OPENSTACK.toLowerCase() }`;
const OVA_SOURCE_TYPE = `${ HCI.MIGRATION }.${ VMIMPORT_SOURCE_KINDS.OVA.toLowerCase() }`;
export default {
name: 'EditVirtualMachineImport',
components: {
CruResource,
Tabbed,
Tab,
LabeledInput,
LabeledSelect,
NameNsDescription,
Checkbox
},
mixins: [CreateEditView, FormValidation],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
},
mode: {
type: String,
required: true,
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
// Fetch all dependencies in parallel to speed up the page load
const hash = {
storageClasses: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
networks: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }),
vmwareSources: this.$store.dispatch(`${ inStore }/findAll`, { type: VMWARE_SOURCE_TYPE }),
openstackSources: this.$store.dispatch(`${ inStore }/findAll`, { type: OPENSTACK_SOURCE_TYPE }),
ovaSources: this.$store.dispatch(`${ inStore }/findAll`, { type: OVA_SOURCE_TYPE }).catch(() => []),
};
const res = await allHash(hash);
this.allStorageClasses = res.storageClasses;
this.allNetworks = res.networks;
this.vmwareSources = res.vmwareSources;
this.openstackSources = res.openstackSources;
this.ovaSources = res.ovaSources;
},
data() {
// Ensure the spec object exists to prevent 'undefined' errors during rendering
if (!this.value.spec) this.value.spec = {};
if (!this.value.spec.sourceCluster) this.value.spec.sourceCluster = {};
if (!this.value.spec.networkMapping) this.value.spec.networkMapping = [];
// Detect if in Edit mode by checking the existing kind
// This allows to pre-select the correct Provider Type tab
let initialProvider = '';
const existingKind = this.value.spec.sourceCluster.kind;
if (existingKind === VMIMPORT_SOURCE_KINDS.VMWARE) initialProvider = VMIMPORT_SOURCE_PROVIDER.VMWARE;
else if (existingKind === VMIMPORT_SOURCE_KINDS.OPENSTACK) initialProvider = VMIMPORT_SOURCE_PROVIDER.OPENSTACK;
else if (existingKind === VMIMPORT_SOURCE_KINDS.OVA) initialProvider = VMIMPORT_SOURCE_PROVIDER.OVA;
// Construct the unique key (Kind/Namespace/Name) if we are editing an existing resource
let initialSourceKey = null;
if (this.value.spec.sourceCluster.name) {
initialSourceKey = `${ existingKind }/${ this.value.spec.sourceCluster.namespace }/${ this.value.spec.sourceCluster.name }`;
}
return {
allStorageClasses: [],
allNetworks: [],
vmwareSources: [],
openstackSources: [],
ovaSources: [],
// UI State
sourceProviderType: initialProvider,
selectedSourceKey: initialSourceKey,
// Static Options
providerTypeOptions: [
{ label: 'VMware', value: VMIMPORT_SOURCE_PROVIDER.VMWARE },
{ label: 'OpenStack', value: VMIMPORT_SOURCE_PROVIDER.OPENSTACK },
{ label: 'OVA', value: VMIMPORT_SOURCE_PROVIDER.OVA }
],
diskBusOptions: [
// Allow resetting selection / reset to the default behavior (sending null/empty)
{ label: this.t('harvester.addons.vmImport.options.useDefault'), value: '' },
{ label: 'VirtIO', value: 'virtio' },
{ label: 'SCSI', value: 'scsi' },
{ label: 'SATA', value: 'sata' },
{ label: 'USB', value: 'usb' },
],
interfaceModelOptions: [
// Allow resetting selection / reset to the default behavior (sending null/empty)
{ label: this.t('harvester.addons.vmImport.options.useDefault'), value: '' },
{ label: 'VirtIO', value: 'virtio' },
{ label: 'e1000', value: 'e1000' },
{ label: 'e1000e', value: 'e1000e' },
{ label: 'ne2k_pci', value: 'ne2k_pci' },
{ label: 'pcnet', value: 'pcnet' },
{ label: 'rtl8139', value: 'rtl8139' },
],
fvFormRuleSets: [
{ path: 'metadata.name', rules: ['nameRequired'] },
{ path: 'spec.virtualMachineName', rules: ['vmNameRequired', 'rfc1123'] },
],
};
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
computed: {
...mapGetters({ t: 'i18n/t' }),
// Return only the sources that match the selected Provider Type (VMware or OpenStack)
sourceOptions() {
let list = [];
if (this.sourceProviderType === VMIMPORT_SOURCE_PROVIDER.VMWARE) {
list = this.vmwareSources;
} else if (this.sourceProviderType === VMIMPORT_SOURCE_PROVIDER.OPENSTACK) {
list = this.openstackSources;
} else if (this.sourceProviderType === VMIMPORT_SOURCE_PROVIDER.OVA) {
list = this.ovaSources;
}
return list.map((s) => {
// Fallback for API version/kind if missing on the object
let kind = s.kind;
if (!kind) {
if (this.sourceProviderType === VMIMPORT_SOURCE_PROVIDER.VMWARE) kind = VMIMPORT_SOURCE_KINDS.VMWARE;
else if (this.sourceProviderType === VMIMPORT_SOURCE_PROVIDER.OPENSTACK) kind = VMIMPORT_SOURCE_KINDS.OPENSTACK;
else if (this.sourceProviderType === VMIMPORT_SOURCE_PROVIDER.OVA) kind = VMIMPORT_SOURCE_KINDS.OVA;
}
const apiVersion = s.apiVersion || `${ HCI.MIGRATION }/v1beta1`;
return {
label: s.metadata.name,
value: `${ kind }/${ s.metadata.namespace }/${ s.metadata.name }`,
// We attach the raw metadata so we can easily populate the spec later without re-finding the object
raw: {
kind,
apiVersion,
name: s.metadata.name,
namespace: s.metadata.namespace
}
};
});
},
fvExtraRules() {
return {
nameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.fields.name') }) : undefined,
vmNameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.vmName') }) : undefined,
rfc1123:
(val) => {
if (val && !isValidDNSLabelName(val)) {
return this.t('harvester.addons.vmImport.errors.rfc1123');
}
return undefined;
}
};
},
// Perform various form validations before allowing to submit
isFormValid() {
// Check VM Name is valid
const nameError = this.fvNameRule(this.value.spec.virtualMachineName);
if (nameError) return false;
// Check mandatory fields in Basics
if (!this.value.spec.virtualMachineName) return false;
if (!this.selectedSourceKey) return false;
// Check Network Mappings
// If any row is missing source or destination, the form is invalid.
const networks = this.value.spec.networkMapping || [];
const hasInvalidRow = networks.some((row) => !row.sourceNetwork || !row.destinationNetwork);
if (hasInvalidRow) return false;
return true;
},
isNetworkTabInvalid() {
const networks = this.value.spec.networkMapping || [];
// Only error if a row exists AND it is missing fields
return networks.some((row) => !row.sourceNetwork || !row.destinationNetwork);
},
// Filter out internal storage classes
// to prevent selecting a class that might cause the import to fail
storageClassOptions() {
return this.allStorageClasses
.filter((sc) => {
const isInternal = sc.parameters?.['harvesterhci.io/isInternalStorageClass'] === 'true';
return !isInternal;
})
.map((sc) => ({
label: sc.nameDisplay,
value: sc.id
}));
},
networkOptions() {
const mgmtOption = {
label: 'Management Network',
value: MANAGEMENT_NETWORK
};
const vlanOptions = this.allNetworks.map((n) => ({
label: n.nameDisplay || n.metadata.name,
value: n.id
}));
return [mgmtOption, ...vlanOptions];
}
},
methods: {
// Clear the selected cluster if the user switches providers (e.g. VMware -> OpenStack)
// Prevents submitting a VMware cluster name while the kind is OpenStack
onProviderTypeChange(newType) {
this.selectedSourceKey = null;
this.value.spec.sourceCluster = {};
},
// Update the sourceCluster object based on the single dropdown selection
updateSource(key) {
this.selectedSourceKey = key;
const selectedOption = this.sourceOptions.find((o) => o.value === key);
if (selectedOption) {
const {
kind, apiVersion, name, namespace
} = selectedOption.raw;
this.value.spec.sourceCluster = {
kind,
apiVersion,
name,
namespace
};
} else {
this.value.spec.sourceCluster = {};
}
},
addNetworkMapping() {
this.value.spec.networkMapping.push({
sourceNetwork: '',
destinationNetwork: '',
networkInterfaceModel: ''
});
},
removeNetworkMapping(index) {
if (!this.value?.spec?.networkMapping) {
return;
}
if (index >= 0 && index < this.value.spec.networkMapping.length) {
this.value.spec.networkMapping.splice(index, 1);
}
},
requiredRule(val) {
if (!val) {
return this.t('validation.required', { key: this.t('generic.value') });
}
return undefined;
},
// Validates that the input follows Kubernetes Naming Rules (RFC 1123).
// If the source VM has uppercase letters or spaces, the user must be warned
// that they cannot import it until they rename it on the source. See:
// https://docs.harvesterhci.io/v1.6/advanced/addons/vmimport/#source-virtual-machine-name-is-not-rfc1123-compliant
fvNameRule(val) {
if (!val) return undefined; // 'Required' check handles empty state separately
// valid RFC 1123
if (!isValidDNSLabelName(val)) {
return this.t('harvester.addons.vmImport.errors.rfc1123');
}
return undefined;
},
updateBeforeSave() {
// If networkMapping exists, filter out the "Management Network" rows
// Let the vm-import-controller set the default network mapping
if (this.value.spec.networkMapping) {
this.value.spec.networkMapping = this.value.spec.networkMapping.filter((row) => {
return row.destinationNetwork !== MANAGEMENT_NETWORK;
});
}
},
// Only handles complex logic that doesn't fit into simple field rules
async saveOverride(buttonCb) {
const errors = [];
this.errors = [];
// Validate Provider Type
if (!this.sourceProviderType) {
errors.push(this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.sourceProvider') }));
}
// Validate Network Tab
if (this.isNetworkTabInvalid) {
errors.push(this.t('harvester.addons.vmImport.errors.networkMappingRequired'));
}
// Return immediately in case of an error, avoid that `this.save()` runs, preventing `updateBeforeSave` from resetting data.
if (errors.length > 0) {
this.errors = errors;
buttonCb(false);
return;
}
// Only proceed if valid
this.save(buttonCb);
},
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
:validation-passed="fvFormIsValid"
@finish="saveOverride"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
:mode="mode"
:rules="{ name: fvGetAndReportPathRules('metadata.name') }"
@update:value="$emit('update:value', $event)"
/>
<Tabbed
v-bind="$attrs"
class="mt-15"
:side-tabs="true"
>
<Tab
name="basic"
:label="t('harvester.addons.vmImport.titles.basic')"
:weight="3"
>
<div class="row mb-20">
<div class="col span-6">
<LabeledSelect
v-model:value="sourceProviderType"
:options="providerTypeOptions"
:label="t('harvester.addons.vmImport.fields.sourceProvider')"
:mode="mode"
:rules="[requiredRule]"
required
@update:value="onProviderTypeChange"
/>
</div>
<div class="col span-6">
<LabeledSelect
:value="selectedSourceKey"
:options="sourceOptions"
:label="t('harvester.addons.vmImport.fields.sourceCluster')"
:placeholder="sourceProviderType ? t('harvester.addons.vmImport.placeholders.selectCluster') : t('harvester.addons.vmImport.placeholders.selectProviderFirst')"
:disabled="!sourceProviderType"
:mode="mode"
:rules="fvGetAndReportPathRules('selectedSourceKey')"
required
@update:value="updateSource"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model:value="value.spec.virtualMachineName"
:label="t('harvester.addons.vmImport.fields.vmName')"
:placeholder="t('harvester.addons.vmImport.placeholders.matchSource')"
:mode="mode"
:rules="fvGetAndReportPathRules('spec.virtualMachineName')"
required
/>
</div>
<div class="col span-6">
<LabeledSelect
v-model:value="value.spec.storageClass"
:options="storageClassOptions"
:label="t('harvester.addons.vmImport.fields.targetStorageClass')"
:mode="mode"
/>
</div>
</div>
</Tab>
<Tab
name="networking"
:label="t('harvester.addons.vmImport.titles.networking')"
:weight="2"
:error="isNetworkTabInvalid"
>
<div
v-for="(row, i) in value.spec.networkMapping"
:key="i"
class="network-row box mb-10"
>
<div class="row">
<div class="col span-4">
<LabeledInput
v-model:value="row.sourceNetwork"
:label="t('harvester.addons.vmImport.fields.sourceNetwork')"
:mode="mode"
:rules="[requiredRule]"
required
/>
</div>
<div class="col span-4">
<LabeledSelect
v-model:value="row.destinationNetwork"
:options="networkOptions"
:label="t('harvester.addons.vmImport.fields.destNetwork')"
:mode="mode"
:rules="[requiredRule]"
required
/>
</div>
<div class="col span-3">
<LabeledSelect
v-model:value="row.networkInterfaceModel"
:options="interfaceModelOptions"
:label="t('harvester.addons.vmImport.fields.interfaceModel')"
:mode="mode"
/>
</div>
<div class="col span-1 remove-btn-container">
<button
type="button"
class="btn role-link"
@click="removeNetworkMapping(i)"
>
{{ t('harvester.addons.vmImport.actions.remove') }}
</button>
</div>
</div>
</div>
<button
type="button"
class="btn role-secondary"
@click="addNetworkMapping"
>
{{ t('harvester.addons.vmImport.actions.addNetwork') }}
</button>
</Tab>
<Tab
name="advanced"
:label="t('harvester.addons.vmImport.titles.advanced')"
:weight="1"
>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model:value="value.spec.folder"
:label="t('harvester.addons.vmImport.fields.folder')"
:placeholder="t('harvester.addons.vmImport.placeholders.folderExample')"
:mode="mode"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledSelect
v-model:value="value.spec.defaultDiskBusType"
:options="diskBusOptions"
:label="t('harvester.addons.vmImport.fields.diskBus')"
:mode="mode"
/>
</div>
<div class="col span-6">
<LabeledSelect
v-model:value="value.spec.defaultNetworkInterfaceModel"
:options="interfaceModelOptions"
:label="t('harvester.addons.vmImport.fields.defaultInterface')"
:mode="mode"
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<Checkbox
v-model:value="value.spec.skipPreflightChecks"
:label="t('harvester.addons.vmImport.fields.skipPreflight')"
:mode="mode"
/>
<Checkbox
v-model:value="value.spec.forcePowerOff"
:label="t('harvester.addons.vmImport.fields.forcePowerOff')"
:mode="mode"
class="mt-10"
/>
</div>
</div>
</Tab>
</Tabbed>
</CruResource>
</template>
<style lang="scss" scoped>
.network-row {
border: 1px solid var(--border);
padding: 10px;
border-radius: var(--border-radius);
background: var(--body-bg);
}
.remove-btn-container {
display: flex;
align-items: center;
justify-content: center;
}
</style>
@@ -0,0 +1,302 @@
<script>
import CruResource from '@shell/components/CruResource';
import Tabbed from '@shell/components/Tabbed';
import Tab from '@shell/components/Tabbed/Tab';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import { RadioGroup } from '@components/Form/Radio';
import CreateEditView from '@shell/mixins/create-edit-view';
import FormValidation from '@shell/mixins/form-validation';
import { SECRET } from '@shell/config/types';
import { randomStr } from '@shell/utils/string';
import { mapGetters } from 'vuex';
export default {
name: 'EditVmwareSource',
// Declare the event, fixes a console warning
emits: ['update:value'],
components: {
CruResource,
Tabbed,
Tab,
LabeledInput,
LabeledSelect,
NameNsDescription,
RadioGroup,
},
mixins: [CreateEditView, FormValidation],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
},
mode: {
type: String,
required: true,
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.allSecrets = await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
},
data() {
if (!this.value.spec) this.value.spec = {};
if (!this.value.spec.credentials) this.value.spec.credentials = {};
const initialMode = this.value.spec.credentials.name ? 'existing' : 'new';
return {
allSecrets: [],
authMode: initialMode,
newUsername: '',
newPassword: '',
newCaCert: '',
fvFormRuleSets: [
{ path: 'metadata.name', rules: ['nameRequired'] },
{ path: 'spec.endpoint', rules: ['endpointRequired'] },
{ path: 'spec.dc', rules: ['dcRequired'] },
],
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
authModeOptions() {
return [
{ label: this.t('harvester.addons.vmImport.fields.createSecret'), value: 'new' },
{ label: this.t('harvester.addons.vmImport.fields.useSecret'), value: 'existing' }
];
},
secretOptions() {
const currentNamespace = this.value.metadata.namespace || 'default';
return this.allSecrets
.filter((s) => s.metadata.namespace === currentNamespace)
.map((s) => ({
label: s.nameDisplay,
value: s.metadata.name
}));
},
// Define custom rules for the FormValidation mixin
fvExtraRules() {
return {
nameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.fields.name') }) : undefined,
endpointRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.vmware.fields.endpoint') }) : undefined,
dcRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.vmware.fields.datacenter') }) : undefined,
};
},
isFormValid() {
if (!this.fvFormIsValid) {
return false;
}
if (this.authMode === 'new') {
if (!this.newUsername || !this.newPassword) return false;
} else {
if (!this.value.spec.credentials.name) return false;
}
return true;
}
},
methods: {
usernameRule(val) {
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.username') }) : undefined;
},
passwordRule(val) {
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.password') }) : undefined;
},
secretRule(val) {
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.selectSecret') }) : undefined;
},
async saveSource(buttonCb) {
const inStore = this.$store.getters['currentProduct'].inStore;
try {
if (this.authMode === 'new') {
const secretName = `${ this.value.metadata.name }-creds-${ randomStr(4).toLowerCase() }`;
const namespace = this.value.metadata.namespace || 'default';
// Create the model with the correct Schema ID (SECRET)
const newSecret = await this.$store.dispatch(`${ inStore }/create`, {
type: SECRET,
metadata: {
name: secretName,
namespace
}
});
// Use '_type' to set the Kubernetes 'type' field.
newSecret['_type'] = 'Opaque';
// base64 encode the data
newSecret['data'] = {
username: btoa(this.newUsername),
password: btoa(this.newPassword),
// Only include CA cert if the user provided one
caCert: this.newCaCert ? btoa(this.newCaCert) : undefined
};
await newSecret.save();
// Link the new secret to the Source
this.value.spec.credentials = {
name: secretName,
namespace
};
}
await this.save(buttonCb);
} catch (err) {
this.errors = [err];
buttonCb(false);
}
}
}
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
:validation-passed="isFormValid"
@finish="saveSource"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
:mode="mode"
:rules="{ name: fvGetAndReportPathRules('metadata.name') }"
@update:value="$emit('update:value', $event)"
/>
<Tabbed
v-bind="$attrs"
class="mt-15"
:side-tabs="true"
>
<Tab
name="basic"
:label="t('harvester.addons.vmImport.titles.basic')"
:weight="2"
>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model:value="value.spec.endpoint"
:label="t('harvester.addons.vmImport.vmware.fields.endpoint')"
:placeholder="t('harvester.addons.vmImport.vmware.placeholders.endpoint')"
:mode="mode"
:rules="fvGetAndReportPathRules('spec.endpoint')"
required
/>
</div>
<div class="col span-6">
<LabeledInput
v-model:value="value.spec.dc"
:label="t('harvester.addons.vmImport.vmware.fields.datacenter')"
:placeholder="t('harvester.addons.vmImport.vmware.placeholders.datacenter')"
:tooltip="t('harvester.addons.vmImport.vmware.tooltips.datacenter')"
:mode="mode"
:rules="fvGetAndReportPathRules('spec.dc')"
required
/>
</div>
</div>
</Tab>
<Tab
name="auth"
:label="t('harvester.addons.vmImport.titles.auth')"
:weight="1"
>
<div class="row mb-20">
<div class="col span-12">
<RadioGroup
v-model:value="authMode"
name="authMode"
:options="authModeOptions"
:mode="mode"
/>
</div>
</div>
<div v-if="authMode === 'new'">
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model:value="newUsername"
:label="t('harvester.addons.vmImport.fields.username')"
:mode="mode"
:rules="[usernameRule]"
required
/>
</div>
<div class="col span-6">
<LabeledInput
v-model:value="newPassword"
type="password"
:label="t('harvester.addons.vmImport.fields.password')"
:mode="mode"
:rules="[passwordRule]"
required
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-12">
<LabeledInput
v-model:value="newCaCert"
type="multiline"
:label="t('harvester.addons.vmImport.fields.caCert')"
:placeholder="t('harvester.addons.vmImport.placeholders.caCert')"
:min-height="100"
:mode="mode"
/>
</div>
</div>
<div class="text-muted">
Note: A new Kubernetes Secret will be created to store these credentials.
</div>
</div>
<div v-if="authMode === 'existing'">
<div class="row mb-20">
<div class="col span-6">
<LabeledSelect
v-model:value="value.spec.credentials.name"
:options="secretOptions"
:label="t('harvester.addons.vmImport.fields.selectSecret')"
:mode="mode"
:rules="[secretRule]"
required
/>
</div>
</div>
</div>
</Tab>
</Tabbed>
</CruResource>
</template>
@@ -0,0 +1,42 @@
<script>
import NetworkPolicy from '@shell/edit/networking.k8s.io.networkpolicy';
import { Banner } from '@components/Banner';
import { DOC } from '../config/doc-links';
import { docLink } from '../utils/feature-flags';
export default {
components: {
NetworkPolicy,
Banner
},
computed: {
networkPolicyDocLink() {
const version = this.$store.getters['harvester-common/getServerVersion']();
return docLink(DOC.NETWORK_POLICY, version);
}
}
};
</script>
<template>
<div class="network-policy-edit">
<Banner color="info">
<t
k="harvester.networkPolicy.banner"
:url="networkPolicyDocLink"
:raw="true"
/>
</Banner>
<NetworkPolicy v-bind="$attrs" />
</div>
</template>
<style lang="scss" scoped>
.network-policy-edit {
flex: auto;
display: flex;
flex-direction: column;
}
</style>
+1
View File
@@ -7,6 +7,7 @@ import harvesterStore from './store/harvester-store';
import customValidators from './validators';
import { PRODUCT_NAME } from './config/harvester';
import { defineAsyncComponent } from 'vue';
import './styles/vue-flow.scss';
// Init the package
export default function (plugin: IPlugin) {
+338 -17
View File
@@ -20,6 +20,7 @@ nav:
Monitoring: Monitoring
Logging: Logging
'Monitoring and Logging': Monitoring and Logging
vmimport: Virtual Machine Imports
resourceTable:
groupBy:
@@ -122,6 +123,24 @@ harvester:
namespace: Namespace
message:
success: 'Image { name } created successfully.'
storageMigration:
title: Storage Migration
fields:
sourceVolume:
label: Source Volume
placeholder: Select a source volume
targetVolume:
label: Target Volume
placeholder: Select a target volume
dataMigration:
title: Data Migration
fields:
targetVolumeName:
label: Target Volume Name
placeholder: Enter a target volume name
targetStorageClassName:
label: Target Storage Class
placeholder: Select a storage class
migration:
failedMessage: Latest migration failed!
title: Migration
@@ -154,15 +173,24 @@ harvester:
nodeTimeout:
label: Node Collection Timeout
tooltip: Minutes allowed for collecting logs/configurations on nodes.<br/>See docs support-bundle-node-collection-timeout for detail.
hotplug:
hotplugVolume:
success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
title: Add Volume
hotplugNic:
success: 'The settings have been saved, but the network interface {interfaceName} will be attached only after the virtual machine is migrated.'
title: Add Network Interface
vmNetwork: Virtual Machine Network
macAddress: MAC Address
macAddressTooltip: If left blank, the MAC address will be automatically generated.
insertCdRomVolume:
success: '{ imageName } is inserted into device { deviceName }.'
title: Insert Image
image: Image
actionLabel: Insert Image
cpuMemoryHotplug:
success: 'CPU and Memory are updated to the virtual machine { vm }.'
title: Edit CPU and Memory
maxResourcesMessage: 'You can increase the CPU to maximum { maxCpu }C and memory to maximum { maxMemory }.'
hotunplug:
success: 'Volume { name } is detached successfully.'
snapshot:
title: Take Snapshot
name: Name
@@ -191,6 +219,9 @@ harvester:
info: Info
warning: Warning
error: Error
restartRequired:
title: '{count} {count, plural, =1 {Virtual Machine is} other {Virtual Machines are}} Pending Restart'
message: 'Please restart { vmNames } to apply updated configurations'
action:
createVM: Create Virtual Machine
start: Start
@@ -219,6 +250,9 @@ harvester:
migrate: Migrate
cpuAndMemoryHotplug: Edit CPU and Memory
abortMigration: Abort Migration
storageMigration: Storage Migration
cancelStorageMigration: Cancel Storage Migration
dataMigration: Data Migration
createTemplate: Generate Template
enableMaintenance: Enable Maintenance Mode
disableMaintenance: Disable Maintenance Mode
@@ -226,7 +260,8 @@ harvester:
disableCPUManager: Disable CPU Manager
cordon: Cordon
uncordon: Uncordon
addHotplug: Add Volume
addHotplugVolume: Add Volume
addHotplugNic: Hotplug Network Interface
exportImage: Export Image
viewlogs: View Logs
cancelExpand: Cancel Expand
@@ -236,6 +271,7 @@ harvester:
shutdown: Shut Down
powerOn: Power On
reboot: Reboot
reconnect: Reconnect
forceStop: Force Stop
formatters:
hardwareResourceGauge:
@@ -271,6 +307,7 @@ harvester:
cronExpression: Cron Expression
retain: Retain
scheduleType: Type
configuredProfiles: Configured Profiles
maxFailure: Max Failure
sourceVm: Source Virtual Machine
vmSchedule: Virtual Machine Schedule
@@ -288,6 +325,17 @@ harvester:
totalSnapshotQuota: Total Snapshot Quota
storageClass: Storage Class
restore: Restore
vmImportSourceVm: Source VM
vmImportSourceCluster: Source Cluster
vmImportStatus: Import Status
vmImportSourceVDatacenter: Datacenter
vmImportSourceVEndpoint: Endpoint
vmImportSourceVClusterStatus: Cluster Status
vmImportSourceORegion: Region
vmImportSourceOEndpoint: Endpoint
vmImportSourceOClusterStatus: Cluster Status
vmImportSourceOVAUrl: URL
vmImportSourceOVAStatus: Status
tab:
volume: Volumes
network: Networks
@@ -328,7 +376,10 @@ harvester:
available: Available Devices
compatibleNodes: Compatible Nodes
impossibleSelection: 'There are no hosts with all of the selected devices.'
howToUseDevice: 'Use the table below to enable PCI passthrough on each device you want to use in this virtual machine.'
howToUseDevice:
prefix: 'Use the table below to enable PCI passthrough on each device you want to use in this virtual machine.<br>For vGPU devices, please enable them on the'
middle: vGPU Devices
suffix: page first.
deviceInTheSameHost: 'You can only select devices on the same host.'
oldFormatDevices:
help: |-
@@ -350,6 +401,9 @@ harvester:
claimError: Error enabling passthrough on {name}
unclaimError: Error disabling passthrough on {name}
cantUnclaim: You cannot disable passthrough on a device claimed by another user.
detachWarning:
title: Cannot Disable Passthrough
message: Please detach the device from the VM and save it first before disabling passthrough.
enableGroup: Enable Group
disableGroup: Disable Group
labelRequired: "This rule should not be manually altered: it ensures that the PCI devices selected for this virtual machine are available on the virtual machine's host."
@@ -395,7 +449,7 @@ harvester:
volume:
upperType: Volume name
lowerType: volume name
needImageOrExisting: 'At least an image volume or an existing root-disk volume is required!'
needAtLeastOneBootable: 'At least one bootable volume is required!'
image:
ruleTip: 'The URL you have entered ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
@@ -403,7 +457,7 @@ harvester:
sha512: 'Invalid SHA512 checksum.'
subnet:
excludeIps: 'Exclude IPs cannot be empty. Please remove or fill in the exclude IPs.'
aclEmptyError: The fields in subnet access control list rule can not be empty.
dashboard:
label: Dashboard
header: "Harvester Cluster: {cluster}"
@@ -440,6 +494,7 @@ harvester:
storage: Storage
labels: Labels
ksmtuned: Ksmtuned
hugepages: Hugepages
seeder: Out-of-band Access
detail:
kvm:
@@ -512,6 +567,20 @@ harvester:
fullScans: Full Scans
stableNodeChains: Stable Node Chains
stableNodeDups: Stable Node Dups
hugepages:
meminfo: Meminfo
transparent:
title: Transparent Hugepages <a href="{url}" target="_blank"><i class="icon icon-info" /></a>
enabled: Enabled
shmemEnabled: Shared Memory Enabled
defrag: Defragmentation
status:
anon: Anonymous Hugepages (bytes)
size: Default Hugepage Size (bytes)
total: Total Hugepages
free: Free Hugepages
rsvd: Reserved Hugepages
surp: Surplus Hugepages
disk:
add: Add Disk
path:
@@ -576,10 +645,28 @@ harvester:
virtualMachine:
label: Virtual Machines
osType: OS Type
cpuModel:
label: CPU Model
fetchError: 'Failed to load CPU model configuration: {error}'
optionLabel: "{modelName} ({count} {count, plural, one {node} other {nodes}})"
hotplug:
title: Enable CPU and memory hotplug
tooltip: The default maximum CPU and maximum memory are {hotPlugTimes} times based on CPU and memory.
restartVMMessage: Restart action is required for the virtual machine configuration change to take effect
hotUnplug:
actionLabel: Detach
detachVolume:
title: 'Are you sure that you want to detach volume {name}?'
actionLabel: Detach Volume
success: 'Volume { name } is detached successfully.'
ejectCdRomVolume:
title: 'Are you sure that you want to eject image from device {name}?'
actionLabel: Eject Image
success: 'Image from device { name } is ejected successfully.'
detachNIC:
title: 'Are you sure that you want to detach network interface {name}?'
actionLabel: Detach Network Interface
success: 'The settings have been saved, but the network interface {name} will be detached only after the virtual machine is migrated.'
instance:
singleInstance:
multipleInstance:
@@ -611,11 +698,6 @@ harvester:
title: 'Select the volume you want to delete:'
deleteAll: Delete All
tips: "Warn: The snapshots of the virtual machine will be deleted with virtual machine and the snapshots of volume will be deleted with volume."
unplug:
title: 'Are you sure that you want to detach volume {name} ?'
actionLabel: Detach
detachVolume:
Detach Volume
restartTip: |-
{restart, select,
true {Restart}
@@ -687,6 +769,7 @@ harvester:
unmount:
title: Are you sure?
message: Are you sure you want to unmount this volume?
emptyImage: No media
network:
title: Network
addNetwork: Add Network
@@ -836,8 +919,14 @@ harvester:
snapshots: Snapshots
datasource: Data Source
details: Details
conditions: Conditions
size: Size
volumeMode: Volume Mode
accessModes: Access Modes
createWithDataVolume: Create with DataVolume
createWithDataVolumeTooltip: Create Volume with Kubevirt/Containerized Data Importer way. It can fill accessMode/volumeMode automatically.
showAdvanced: Show Advanced Options
hideAdvanced: Hide Advanced Options
source: Source
kind: Kind
sourceOptions:
@@ -894,6 +983,8 @@ harvester:
checksumTip: Validate the image using the SHA512 checksum, if specified.
tooltip:
imported: Created automatically by the vm-import-controller
errors:
unsupportedBackend: 'Unsupported backend type: {backend}'
vmTemplate:
label: Templates
@@ -955,7 +1046,9 @@ harvester:
createTitle: Create Schedule
createButtonText: Create Schedule
scheduleType: Virtual Machine Schedule Type
cron: Cron Schedule
cron:
label: Cron Schedule
editButton: Edit
detail:
namespace: Namespace
sourceVM: Source Virtual Machine
@@ -1044,6 +1137,13 @@ harvester:
gateway:
label: Gateway IP
placeholder: e.g. 172.20.0.1
dhcp:
label: Dynamic Host Configuration Protocol (DHCP)
v4Options: DHCPV4Options
v6Options: DHCPV6Options
placeholder: key1=value1, key2=value2
dhcpOptionBanner: DHCP options is a key/value string concatenate with comma. For more detail, please refer to <a href="https://kubeovn.github.io/docs/v1.13.x/en/kubevirt/dhcp/" target="_blank">KubOVN document</a>
tooltip: Enable DHCP server for this subnet. When enabled, VMs can automatically obtain IP addresses from this subnet.
private:
label: Private Subnet
tooltip: Enable network isolation for this Subnet. When enabled, VMs can only communicate within this subnet, even if other subnets exist under the same VPC.
@@ -1054,8 +1154,46 @@ harvester:
placeholder: e.g. 172.16.0.0/16
excludeIPs:
tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list.
placeholder: single IP or 192.168.0.100..192.168.0.200
acl:
label: Access Control List
tooltip: The ACL to apply to this Subnet. Must be one of the ACLs in the same namespace.
action:
label: Action
placeholder: Please select an action
direction:
label: Direction
placeholder: Please select a direction
addRule: Add Rule
priority:
label: Priority
placeholder: Please select a priority
match:
label: Match
placeholder: e.g. ip4.dst == 10.10.0.2
banner: The supported field in ACL match can refer to <a href="https://kubeovn.github.io/docs/v1.14.x/en/guide/subnet/#subnet-acl" target="_blank">KubeOvn Subnet ACL document</a>
vpc:
viewTopology: Topology
topology:
loading: Loading topology...
empty: No resources found
visibility:
vpc: VPC
subnets: Subnets
overlayNetworks: Overlay Networks
vms: VMs
labels:
cidr: CIDR
provider: Provider
type: Type
clusterNetwork: Cluster Network
network: Network
subnet: Subnet
ip: IP
mac: MAC
peering: Peering
noAddonEnabled:
prefix: The kubeovn-operator add-on is not enabled, click
middle: here
@@ -1080,6 +1218,10 @@ harvester:
placeholder: e.g. 169.254.0.1/16
remoteVpc:
label: Remote VPC
infoBanner: The static route destination CIDR must cover all subnets CIDR from remote VPC Peer. Read <a href="{url}" target="_blank">VPC Peering Configuration Examples</a> for more information.
networkPolicy:
label: Network Policies
banner: The network policies must be used for VMs attached to overlay networks. Please read the <a href="{url}" target="_blank">harvester document</a> how the network policy works.
network:
label: Virtual Machine Networks
tabs:
@@ -1148,13 +1290,19 @@ harvester:
vlan: VLAN ID
exclude:
label: Exclude IPs
placeholder: e.g. 172.16.0.1
placeholder: CIDR format, e.g. 172.16.0.10/32
invalid: '"Exclude list" is invalid.'
addIp: Add Exclude IP
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
rwxNetwork:
warning: 'WARNING: <br/> Any change to rwx-network requires longhorn RWX volumes detached before applying this setting.<br/>Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
shareStorageNetwork: Share Storage Network
dedicatedRwxNetwork: Dedicated RWX Network
shareStorageNetworkWarning: The rwx-network is governed by storage-network, and changes here won't take effect until share-storage-network is set to false.
vmForceDeletionPolicy:
period: Period
vmMigrationTimeout: VM Migration Timeout
vmMigrationNetwork:
parseError: "Failed to parse existing configuration."
fetchError: "Failed to load required network resources: {error}. Please refresh the page or try again later."
@@ -1218,7 +1366,10 @@ harvester:
deleteImage: Please select an image to delete.
deleteSuccess: "{name} deleted successfully."
imagePreloadStrategy: Image Preload Strategy
nodeUpgradeOption: Node Upgrade Option
restoreVM: Restore VM
strategy: Strategy
pauseNodes: Pause Nodes
strategyType: Strategy Type
concurrency: Concurrency
harvesterMonitoring:
@@ -1234,10 +1385,35 @@ harvester:
retention: How long to retain metrics
retentionSize: Maximum size of metrics
clusterRegistrationUrl:
url: URL
insecureSkipTLSVerify: Insecure Skip TLS Verify
tip:
prefix: Harvester secures cluster registration via TLS by default. If opt out "Insecure Skip TLS Verify", you must provide custom CA certificates using the
middle: 'additional-ca'
suffix: setting.
message: To completely unset the imported Harvester cluster, please also remove it on the Rancher Dashboard UI via the <code> Virtualization Management </code> page.
ntpServers:
isNotIPV4: The address you entered is not IPv4 or host. Please enter a valid IPv4 address or a host address.
isDuplicate: There are duplicate NTP server configurations.
instanceManagerResources:
parseError: "Failed to parse configuration: {error}"
v1: "V1 Data Engine"
v2: "V2 Data Engine"
kubevirtMigration:
parseError: "Failed to parse configuration: {error}"
parallelMigrationsPerCluster: "Parallel Migrations Per Cluster"
parallelOutboundMigrationsPerNode: "Parallel Outbound Migrations Per Node"
bandwidthPerMigration: "Bandwidth Per Migration"
bandwidthPerMigrationTooltip: '0 is KubeVirt default, representing unlimited bandwidth'
completionTimeoutPerGiB: "Completion Timeout Per GiB"
progressTimeout: "Progress Timeout"
allowAutoConverge: "Allow Auto Converge"
allowPostCopy: "Allow Post Copy"
unsafeMigrationOverride: "Unsafe Migration Override"
allowWorkloadDisruption: "Allow Workload Disruption"
disableTLS: "Disable TLS"
matchSELinuxLevelOnMigration: "Match SELinux Level On Migration"
cloudTemplate:
label: Cloud Configuration Templates
templateType: Template Type
@@ -1453,6 +1629,14 @@ harvester:
vlanStatus:
vlanConfig:
label: Network Configuration
l2VlanMode:
access: Access
trunk: Trunk
label: Mode
vlanTrunk:
minId: Minimum VLAN ID
maxId: Maximum VLAN ID
add: Add VLAN Trunk
clusterNetwork:
title: Cluster Network Configuration
@@ -1480,10 +1664,86 @@ harvester:
'harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
'harvester-system/harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
'harvester-csi-driver-lvm': harvester-csi-driver-lvm is an add-on allowing users to create PVC through the LVM with local devices.
'descheduler': 'The virtual machine auto balance optimizes workload scheduling by evicting pods that are not optimally placed according to administrator-defined policies.'
vmImport:
titles:
basic: Basic
auth: Authentication
pvc: Volume
networking: Network Mapping
advanced: Advanced
labels:
vmimport: Virtual Machine Import
vmimportSourceVMWare: Source VMWare
vmimportSourceOpenStack: Source OpenStack
vmimportSourceOVA: Source OVA
fields:
sourceProvider: Source Provider Type
sourceCluster: Source Cluster
vmName: VM Name
targetStorageClass: Target Storage Class
sourceNetwork: Source Network Name
destNetwork: Destination Network
interfaceModel: Interface Model
folder: Folder
diskBus: Default Disk Bus
defaultInterface: Default Network Interface
skipPreflight: Skip Preflight Checks
forcePowerOff: Force Power Off Source VM
username: Username
password: Password
caCert: CA Certificate (PEM)
selectSecret: Select Secret
createSecret: Create New Credentials
useSecret: Use Existing Secret
none: None (Public URL)
placeholders:
selectCluster: Select a cluster...
selectProviderFirst: Select a provider type first
matchSource: Must match the name in the source cluster
folderExample: e.g. /Datacenters/DC1/vm
caCert: "-----BEGIN CERTIFICATE----- ..."
options:
useDefault: Use Default
actions:
addNetwork: Add Network Mapping
remove: Remove
errors:
rfc1123: 'Invalid format. Name must be lowercase, alphanumeric, and cannot contain spaces (e.g. "my-vm-1"). If your Source VM name does not match this, you must rename it on the Source cluster first.'
networkMappingRequired: Every Network Mapping row must have a Source and Destination selected.
openstack:
fields:
endpoint: Identity Service Endpoint
region: Region
projectName: Project Name
domainName: Domain Name
retryCount: Upload Image Retry Count
retryDelay: Upload Image Retry Delay
placeholders:
endpoint: "e.g. https://devstack/identity"
region: e.g. RegionOne
projectName: e.g. admin
domainName: e.g. default
retryCount: "Default: 30"
retryDelay: "Default: 10"
vmware:
fields:
endpoint: vCenter Endpoint
datacenter: Datacenter
placeholders:
endpoint: "e.g. https://vscim/sdk"
datacenter: e.g. DC0
tooltips:
datacenter: The exact name of the Datacenter object in vCenter
ova:
fields:
url: URL
httpTimeout: HTTP Timeout
placeholders:
url: "e.g. https://download.example.com/images/my-vm.ova"
httpTimeout: "Default: 600"
rancherVcluster:
accessRancher: Access the Rancher Dashboard
hostname: Hostname
@@ -1610,7 +1870,8 @@ harvester:
numVFs: Number Of Virtual Functions
vfAddresses: Virtual Functions Addresses
showMore: Show More
parentSriov: Filter By Parent SR-IOV
parentSriov: Filter By Parent SR-IOV Netork Device
parentSriovGPU: Filter By Parent SR-IOV GPU Device
sriovgpu:
label: SR-IOV GPU Devices
@@ -1626,9 +1887,32 @@ harvester:
middle: here
suffix: to enable it to manage your SR-IOV GPU devices.
migconfiguration:
label: vGPU MIG Configurations
infoBanner: To configure the MIG configuration, please disable it first and re-enable after editing the configuration.
profileSpec: Profile Specs
profileStatus: Profile Status
tableHeaders:
profileName: Profile Name
total: Total
vGPUID: vGPU ID
available: Available
requested: Requested
requested: Requested
available: Available
total: Total
vGPUID: vGPU ID
goSriovGPU:
prefix: Please enable the supported GPU devices in
middle: SR-IOV GPU Devices
suffix: page to manage the vGPU MIG configurations.
vgpu:
label: vGPU Devices
noPermission: Please contact system administrator to add Harvester add-ons first.
detachWarning:
title: Cannot Disable vGPU
message: Please detach the device from the VM and save it first before disabling this vGPU device.
goSetting:
prefix: The nvidia-driver-toolkit add-on is not enabled, click
middle: here
@@ -1663,6 +1947,9 @@ harvester:
claimError: Error enabling passthrough on {name}
unclaimError: Error disabling passthrough on {name}
cantUnclaim: You cannot disable passthrough on a device claimed by another user.
detachWarning:
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.'
harvesterVlanConfigMigrateDialog:
@@ -1718,19 +2005,20 @@ advancedSettings:
'harv-server-version': Harvester server version.
'harv-upgrade-checker-enabled': Specifies whether to enable Harvester upgrade check or not. Default is True.
'harv-upgrade-checker-url': Default Harvester upgrade check url. Only used when the <code>upgrade-checker-enabled</code> is equal to True.
'harv-ui-source': Configure how to load the UI source.
'harv-ui-source': Configure how to load the UI source. The `External` option is used to load the UI from a ui-index URL, while the `Bundled` option loads the UI from the local Harvester server.
'harv-ui-index': 'HTML index location for the UI.'
'harv-ui-plugin-index': 'JS index location for the Harvester plugin UI.'
'harv-cluster-registration-url': Registration URL for multi-cluster management.
'harv-http-proxy': 'HTTP proxy for Harvester to access external services.'
'harv-additional-ca': 'Custom CA root certificates for TLS validation.'
'harv-overcommit-config': 'Resource overcommit configuration.'
'harv-support-bundle-timeout': 'Support bundle timeout configuration in minutes, use 0 to disable the timeout.'
'harv-support-bundle-file-name': 'Support bundle file name configuration.'
'harv-support-bundle-expiration': 'Support bundle expiration configuration in minutes.'
'harv-support-bundle-node-collection-timeout': 'Support bundle node collection timeout configuration in minutes.'
'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down.
'harv-ssl-parameters': Custom SSL Parameters for TLS validation.
'harv-storage-network': 'Longhorn storage-network setting.'
'harv-rwx-network': 'Configure RWX network behavior for shared or dedicated storage network usage.'
'harv-support-bundle-namespaces': Select additional namespaces to include in the support bundle.
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage.
'harv-support-bundle-image': Support bundle image configuration. Find different versions in <a href="https://hub.docker.com/r/rancher/support-bundle-kit/tags" target="_blank">rancher/support-bundle-kit</a>.
@@ -1745,6 +2033,8 @@ advancedSettings:
'harv-vm-migration-network': 'Segregated network for VM migration traffic.'
'harv-rancher-cluster': 'Configure Rancher cluster integration settings for guest cluster management.'
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
'harv-kubevirt-migration': 'Configure cluster-wide KubeVirt live migration parameters.'
'harv-instance-manager-resources': 'Configure resource percentage reservations for Longhorn instance manager V1 and V2. Valid instance manager CPU range between 0 - 40.'
typeLabel:
kubevirt.io.virtualmachine: |-
@@ -1807,6 +2097,11 @@ typeLabel:
one { Virtual Private Cloud }
other { Virtual Private Clouds }
}
networking.k8s.io.networkpolicy: |-
{count, plural,
one { Network Policy }
other { Network Policies }
}
harvesterhci.io.cloudtemplate: |-
{count, plural,
one { Cloud Configuration Template }
@@ -1895,6 +2190,12 @@ typeLabel:
other { vGPU Devices }
}
devices.harvesterhci.io.migconfiguration: |-
{count, plural,
one { vGPU MIG Configuration }
other { vGPU MIG Configurations }
}
harvesterhci.io.secret: |-
{count, plural,
one { Secret }
@@ -1915,3 +2216,23 @@ typeLabel:
one { IP Pool }
other { IP Pools }
}
migration.harvesterhci.io.openstacksource: |-
{count, plural,
one { OpenStack Source }
other { OpenStack Sources }
}
migration.harvesterhci.io.vmwaresource: |-
{count, plural,
one { VMware Source }
other { VMware Sources }
}
migration.harvesterhci.io.ovasource: |-
{count, plural,
one { OVA Source }
other { OVA Sources }
}
migration.harvesterhci.io.virtualmachineimport: |-
{count, plural,
one { Virtual Machine Import }
other { Virtual Machine Imports }
}
@@ -0,0 +1,177 @@
<script>
import { STATE, NAME } from '@shell/config/table-headers';
import { allHash } from '@shell/utils/promise';
import Banner from '@components/Banner/Banner.vue';
import Loading from '@shell/components/Loading';
import ResourceTable from '@shell/components/ResourceTable';
import { HCI } from '../types';
import { ADD_ONS } from '../config/harvester-map';
import MessageLink from '@shell/components/MessageLink';
export default {
name: 'ListMIGConfigurations',
inheritAttrs: false,
components: {
Banner,
Loading,
ResourceTable,
MessageLink,
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.schema = this.$store.getters[`${ inStore }/schemaFor`](HCI.MIG_CONFIGURATION);
this.hasAddonSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.ADD_ONS);
if (this.hasSchema) {
try {
const hash = await allHash({
migconfigs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.MIG_CONFIGURATION }),
vGpuDevices: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VGPU_DEVICE }),
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS })
});
this.hasPCIAddon = hash.addons.find((addon) => addon.name === ADD_ONS.PCI_DEVICE_CONTROLLER)?.spec?.enabled === true;
this.hasSriovgpuAddon = hash.addons.find((addon) => addon.name === ADD_ONS.NVIDIA_DRIVER_TOOLKIT_CONTROLLER)?.spec?.enabled === true;
this.hasSRIOVGPUSchema = !!this.$store.getters[`${ inStore }/schemaFor`](HCI.SR_IOVGPU_DEVICE);
if (this.hasSRIOVGPUSchema) {
await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOVGPU_DEVICE });
}
this.rows = hash.migconfigs;
} catch (e) {}
}
},
data() {
return {
rows: [],
schema: null,
hasAddonSchema: false,
hasPCIAddon: false,
hasSriovgpuAddon: false,
hasSRIOVGPUSchema: false,
toVGpuAddon: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.NVIDIA_DRIVER_TOOLKIT_CONTROLLER }?mode=edit`,
toPciAddon: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.PCI_DEVICE_CONTROLLER }?mode=edit`,
SRIOVGPUPage: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.NVIDIA_DRIVER_TOOLKIT_CONTROLLER }?mode=edit`,
};
},
computed: {
hasSchema() {
return !!this.schema;
},
rowsData() {
const inStore = this.$store.getters['currentProduct'].inStore;
const rows = this.$store.getters[`${ inStore }/all`](HCI.MIG_CONFIGURATION) || [];
return rows;
},
sriovGPUPage() {
return {
name: 'harvester-c-cluster-resource',
params: { cluster: this.$store.getters['clusterId'], resource: HCI.SR_IOVGPU_DEVICE },
};
},
showEnableSRIOVGPUMessage() {
return this.rowsData.length === 0;
},
headers() {
const cols = [
STATE,
NAME,
{
name: 'address',
label: 'Address',
value: 'spec.gpuAddress',
sort: ['spec.gpuAddress']
},
{
name: 'Configured Profile',
label: 'Configured Count',
labelKey: 'harvester.tableHeaders.configuredProfiles',
value: 'configuredProfiles',
sort: ['configuredProfiles'],
align: 'center'
},
{
name: 'status',
label: 'Status',
labelKey: 'tableHeaders.status',
sort: ['status.status'],
value: 'status.status',
},
];
return cols;
},
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else-if="!hasAddonSchema">
<Banner color="warning">
{{ t('harvester.vgpu.noPermission') }}
</Banner>
</div>
<div v-else-if="!hasSriovgpuAddon || !hasPCIAddon">
<Banner
v-if="!hasSriovgpuAddon"
color="warning"
>
<MessageLink
:to="toVGpuAddon"
prefix-label="harvester.vgpu.goSetting.prefix"
middle-label="harvester.vgpu.goSetting.middle"
suffix-label="harvester.vgpu.goSetting.suffix"
/>
</Banner>
<Banner
v-if="!hasPCIAddon"
color="warning"
>
<MessageLink
:to="toPciAddon"
prefix-label="harvester.pci.goSetting.prefix"
middle-label="harvester.pci.goSetting.middle"
suffix-label="harvester.pci.goSetting.suffix"
/>
</Banner>
</div>
<div v-else-if="hasSchema">
<Banner
v-if="showEnableSRIOVGPUMessage"
color="warning"
>
<MessageLink
:to="sriovGPUPage"
prefix-label="harvester.migconfiguration.goSriovGPU.prefix"
middle-label="harvester.migconfiguration.goSriovGPU.middle"
suffix-label="harvester.migconfiguration.goSriovGPU.suffix"
/>
</Banner>
<Banner
v-if="!showEnableSRIOVGPUMessage"
color="warning"
:label="t('harvester.migconfiguration.infoBanner')"
/>
<ResourceTable
v-bind="$attrs"
:groupable="false"
:namespaced="false"
:headers="headers"
:schema="schema"
:rows="rowsData"
key-field="_key"
/>
</div>
</template>
@@ -64,6 +64,10 @@ export default {
const inStore = this.$store.getters['currentProduct'].inStore;
const rows = this.$store.getters[`${ inStore }/all`](HCI.PCI_DEVICE);
rows.forEach((row) => {
row.allowDisable = true;
});
return rows;
}
},
@@ -54,7 +54,13 @@ export default {
devices() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/all`](HCI.USB_DEVICE) || [];
const data = this.$store.getters[`${ inStore }/all`](HCI.USB_DEVICE) || [];
data.forEach((row) => {
row.allowDisable = true;
});
return data;
}
},
@@ -65,6 +65,10 @@ export default {
const vGpuDevices = this.$store.getters[`${ inStore }/all`](HCI.VGPU_DEVICE) || [];
const srioVGpuDevices = this.$store.getters[`${ inStore }/all`](HCI.SR_IOVGPU_DEVICE) || [];
vGpuDevices.forEach((row) => {
row.allowDisable = true;
});
if (this.hasSRIOVGPUSchema) {
return vGpuDevices.filter((device) => !!srioVGpuDevices.find((s) => s.isEnabled && s.spec?.nodeName === device.spec?.nodeName));
}
@@ -4,7 +4,7 @@ import Loading from '@shell/components/Loading';
import MessageLink from '@shell/components/MessageLink';
import Masthead from '@shell/components/ResourceList/Masthead';
import ResourceTable from '@shell/components/ResourceTable';
import { isBackupTargetSettingEmpty } from '../utils/setting';
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
import FilterVMSchedule from '../components/FilterVMSchedule';
import { HCI } from '../types';
@@ -39,7 +39,7 @@ export default {
this.settings = hash.settings;
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.SETTING)) {
const backupTargetResource = hash.settings.find( (O) => O.id === 'backup-target');
const isEmpty = this.getBackupTargetValueIsEmpty(backupTargetResource);
const isEmpty = isBackupTargetSettingEmpty(backupTargetResource);
if (backupTargetResource && !isEmpty) {
this.testConnect();
@@ -78,20 +78,6 @@ export default {
}
},
getBackupTargetValueIsEmpty(resource) {
let out = true;
if (resource?.value) {
try {
const valueJson = JSON.parse(resource?.value);
out = !valueJson.type;
} catch (e) {}
}
return out;
},
getRow(row) {
return row.status && row.status.source;
},
@@ -183,11 +169,9 @@ export default {
backupTargetResource() {
return this.settings.find((O) => O.id === 'backup-target');
},
isEmptyValue() {
return this.getBackupTargetValueIsEmpty(this.backupTargetResource);
return isBackupTargetSettingEmpty(this.backupTargetResource);
},
canUpdate() {
return this?.backupTargetResource?.canUpdate;
},
@@ -70,8 +70,13 @@ export default {
return schema;
},
filterRows() {
// we only show the non golden image PVCs in the list
return this.rows.filter((pvc) => !pvc?.isGoldenImageVolume);
return this.rows.filter((pvc) => {
if (pvc?.isGoldenImageVolume || pvc?.isCDIPopulatorVolume) {
return false;
}
return true;
});
},
headers() {
return [
+17
View File
@@ -155,6 +155,15 @@ export default {
return location;
},
viewTopology(group) {
const vpc = group.key;
const resource = this.$store.getters[`harvester/byId`](HCI.VPC, vpc);
if (resource && resource.goToDetail) {
resource.goToDetail();
}
},
showVpcAction(event, group) {
const vpc = group.key;
@@ -218,6 +227,14 @@ export default {
>
{{ t('harvester.vpc.createSubnet') }}
</router-link>
<button
type="button"
class="btn btn-sm role-secondary mr-5"
@click="viewTopology(group)"
>
<i class="icon icon-globe mr-5" />
{{ t('harvester.vpc.viewTopology') }}
</button>
<button
type="button"
class="btn btn-sm role-multi-action actions mr-10"
@@ -74,7 +74,7 @@ export default {
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const _hash = {
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM, opt: { force: true } }),
pod: this.$store.dispatch(`${ inStore }/findAll`, { type: POD }),
pvcs: this.$store.dispatch(`${ inStore }/findAll`, { type: PVC }),
pvs: this.$store.dispatch(`${ inStore }/findAll`, { type: PV }),
@@ -110,11 +110,12 @@ export default {
data() {
return {
hasNode: false,
allVMs: [],
allVMIs: [],
allNodeNetworks: [],
allClusterNetworks: [],
hasNode: false,
allVMs: [],
allVMIs: [],
allNodeNetworks: [],
allClusterNetworks: [],
restartNotificationDisplayed: false,
HCI
};
},
@@ -174,6 +175,48 @@ export default {
this['allVMIs'] = vmis;
},
beforeUnmount() {
// clear restart message before component unmount
this.$store.dispatch('growl/clear');
},
watch: {
allVMs: {
handler(neu) {
const vmNames = [];
neu.forEach((vm) => {
if (vm.isRestartRequired) {
vmNames.push(vm.metadata.name);
}
});
const count = vmNames.length;
if ( count === 0 && this.restartNotificationDisplayed) {
this.restartNotificationDisplayed = false;
return;
}
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,
}
},
methods: {
lockIconTooltipMessage(row) {
const message = '';
@@ -243,6 +286,12 @@ export default {
</div>
</template>
<style lang="scss">
.growl-container {
z-index: 56 !important; // set to be lower than the vm action menu (z-index: 57)
}
</style>
<style lang="scss" scoped>
.state {
display: flex;
@@ -0,0 +1,60 @@
<script>
import ResourceTable from '@shell/components/ResourceTable';
import Loading from '@shell/components/Loading';
import { SCHEMA } from '@shell/config/types';
import { HCI } from '../types';
const schema = {
id: HCI.VMIMPORT_SOURCE_O,
type: SCHEMA,
attributes: {
kind: HCI.VMIMPORT_SOURCE_O,
namespaced: true
},
metadata: { name: HCI.VMIMPORT_SOURCE_O },
};
export default {
name: 'HarvesterVMImportSourceO',
components: { ResourceTable, Loading },
inheritAttrs: false,
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIMPORT_SOURCE_O });
const configSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.VMIMPORT_SOURCE_O);
if (!configSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) {
this.$store.dispatch('type-map/configureType', { match: HCI.VMIMPORT_SOURCE_O, isCreatable: false });
}
},
data() {
return { rows: [] };
},
computed: {
schema() {
return schema;
}
},
typeDisplay() {
return this.$store.getters['type-map/labelFor'](schema, 99);
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<ResourceTable
v-else
v-bind="$attrs"
:groupable="true"
:schema="schema"
:rows="rows"
key-field="_key"
/>
</template>
@@ -0,0 +1,60 @@
<script>
import ResourceTable from '@shell/components/ResourceTable';
import Loading from '@shell/components/Loading';
import { SCHEMA } from '@shell/config/types';
import { HCI } from '../types';
const schema = {
id: HCI.VMIMPORT_SOURCE_OVA,
type: SCHEMA,
attributes: {
kind: HCI.VMIMPORT_SOURCE_OVA,
namespaced: true
},
metadata: { name: HCI.VMIMPORT_SOURCE_OVA },
};
export default {
name: 'HarvesterVMImportSourceOVA',
components: { ResourceTable, Loading },
inheritAttrs: false,
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIMPORT_SOURCE_OVA });
const configSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.VMIMPORT_SOURCE_OVA);
if (!configSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) {
this.$store.dispatch('type-map/configureType', { match: HCI.VMIMPORT_SOURCE_OVA, isCreatable: false });
}
},
data() {
return { rows: [] };
},
computed: {
schema() {
return schema;
}
},
typeDisplay() {
return this.$store.getters['type-map/labelFor'](schema, 99);
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<ResourceTable
v-else
v-bind="$attrs"
:groupable="true"
:schema="schema"
:rows="rows"
key-field="_key"
/>
</template>
@@ -0,0 +1,60 @@
<script>
import ResourceTable from '@shell/components/ResourceTable';
import Loading from '@shell/components/Loading';
import { SCHEMA } from '@shell/config/types';
import { HCI } from '../types';
const schema = {
id: HCI.VMIMPORT,
type: SCHEMA,
attributes: {
kind: HCI.VMIMPORT,
namespaced: true
},
metadata: { name: HCI.VMIMPORT },
};
export default {
name: 'HarvesterVMImportVirtualMachine',
components: { ResourceTable, Loading },
inheritAttrs: false,
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIMPORT });
const configSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.VMIMPORT);
if (!configSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) {
this.$store.dispatch('type-map/configureType', { match: HCI.VMIMPORT, isCreatable: false });
}
},
data() {
return { rows: [] };
},
computed: {
schema() {
return schema;
}
},
typeDisplay() {
return this.$store.getters['type-map/labelFor'](schema, 99);
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<ResourceTable
v-else
v-bind="$attrs"
:groupable="true"
:schema="schema"
:rows="rows"
key-field="_key"
/>
</template>
@@ -0,0 +1,60 @@
<script>
import ResourceTable from '@shell/components/ResourceTable';
import Loading from '@shell/components/Loading';
import { SCHEMA } from '@shell/config/types';
import { HCI } from '../types';
const schema = {
id: HCI.VMIMPORT_SOURCE_V,
type: SCHEMA,
attributes: {
kind: HCI.VMIMPORT_SOURCE_V,
namespaced: true
},
metadata: { name: HCI.VMIMPORT_SOURCE_V },
};
export default {
name: 'HarvesterVMImportSourceV',
components: { ResourceTable, Loading },
inheritAttrs: false,
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIMPORT_SOURCE_V });
const configSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.VMIMPORT_SOURCE_V);
if (!configSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) {
this.$store.dispatch('type-map/configureType', { match: HCI.VMIMPORT_SOURCE_V, isCreatable: false });
}
},
data() {
return { rows: [] };
},
computed: {
schema() {
return schema;
}
},
typeDisplay() {
return this.$store.getters['type-map/labelFor'](schema, 99);
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<ResourceTable
v-else
v-bind="$attrs"
:groupable="true"
:schema="schema"
:rows="rows"
key-field="_key"
/>
</template>
+112 -31
View File
@@ -22,7 +22,7 @@ import {
} from '../../config/harvester-map';
import { HCI_SETTING } from '../../config/settings';
import { HCI } from '../../types';
import { parseVolumeClaimTemplates } from '../../utils/vm';
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';
@@ -142,6 +142,7 @@ export default {
showYaml: false,
spec: null,
osType: 'linux',
useTemplate: false,
sshKey: [],
maintenanceStrategies,
maintenanceStrategy: 'Migrate',
@@ -181,6 +182,7 @@ export default {
immutableMode: this.realMode === _CREATE ? _CREATE : _VIEW,
terminationGracePeriodSeconds: '',
cpuPinning: false,
cpuModel: '',
};
},
@@ -273,7 +275,7 @@ export default {
needNewSecret() {
// When creating a template it is always necessary to create a new secret.
return this.showYaml ? false : this.resourceType === HCI.VM_VERSION || this.isCreate;
return this.isCreate || this.showYaml ? false : this.resourceType === HCI.VM_VERSION;
},
defaultTerminationSetting() {
@@ -299,9 +301,11 @@ export default {
async created() {
await this.$store.dispatch(`${ this.inStore }/findAll`, { type: SECRET });
if (this.value.vmMachineTypesFeatureEnabled) {
if (!this.value.vmMachineTypeAutoFeatureEnabled) {
try {
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/clusters/local?link=machineTypes');
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl'](
'v1/harvester/clusters/local?link=machineTypes'
);
const machineTypes = await this.$store.dispatch('harvester/request', { url });
this.machineTypes = machineTypes;
@@ -391,6 +395,7 @@ export default {
const efiPersistentStateEnabled = this.isEFIPersistentStateEnabled(spec);
const secureBoot = this.isSecureBoot(spec);
const cpuPinning = this.isCpuPinning(spec);
const cpuModel = spec.template.spec.domain.cpu?.model || '';
const secretRef = this.getSecret(spec);
const accessCredentials = this.getAccessCredentials(spec);
@@ -428,6 +433,7 @@ export default {
this['tpmPersistentStateEnabled'] = tpmPersistentStateEnabled;
this['secureBoot'] = secureBoot;
this['cpuPinning'] = cpuPinning;
this['cpuModel'] = cpuModel;
this['hasCreateVolumes'] = hasCreateVolumes;
this['networkRows'] = networkRows;
@@ -505,12 +511,15 @@ export default {
const type = DISK?.cdrom ? CD_ROM : DISK?.disk ? HARD_DISK : '';
if (volume?.containerDisk) { // SOURCE_TYPE.CONTAINER
if (type === CD_ROM && volume === undefined) {
// Empty CD_ROM
source = SOURCE_TYPE.IMAGE;
image = EMPTY_IMAGE;
size = `0${ GIBIBYTE }`;
} else if (volume.containerDisk) { // SOURCE_TYPE.CONTAINER
source = SOURCE_TYPE.CONTAINER;
container = volume.containerDisk.image;
}
if (volume.persistentVolumeClaim && volume.persistentVolumeClaim?.claimName) {
} else if (volume.persistentVolumeClaim && volume.persistentVolumeClaim?.claimName) {
volumeName = volume.persistentVolumeClaim.claimName;
const DVT = _volumeClaimTemplates.find( (T) => T.metadata.name === volumeName);
@@ -687,7 +696,6 @@ export default {
set(this.spec.template.spec, 'domain.memory.maxGuest', this.maxMemory);
set(this.spec.template.spec, 'domain.resources.limits.memory', this.maxMemory);
} else {
this.spec.template.spec.domain.cpu.maxSockets = 1;
this.spec.template.spec.domain.cpu.sockets = 1;
this.spec.template.spec.domain.cpu.cores = this.cpu;
this.spec.template.spec.domain.resources.limits.cpu = this.cpu?.toString();
@@ -699,34 +707,41 @@ export default {
}
},
needVolumeRelatedInfo(R) {
// return [needVolume, needVolumeClaimTemplate]
if (R.source === SOURCE_TYPE.CONTAINER) {
return [true, false];
}
if (R.source === SOURCE_TYPE.IMAGE && R.image === EMPTY_IMAGE) {
return [false, false];
}
return [true, true];
},
parseDiskRows(disk) {
const disks = [];
const volumes = [];
const diskNameLables = [];
const volumeClaimTemplates = [];
disk.forEach( (R, index) => {
const prefixName = this.value.metadata?.name || '';
let dataVolumeName = '';
if (R.source === SOURCE_TYPE.ATTACH_VOLUME) {
dataVolumeName = R.volumeName;
} else if (this.isClone || !this.hasCreateVolumes.includes(R.realName)) {
dataVolumeName = `${ prefixName }-${ R.name }-${ randomStr(5).toLowerCase() }`;
} else {
dataVolumeName = R.realName;
}
const _disk = this.parseDisk(R, index);
const _volume = this.parseVolume(R, dataVolumeName);
const _dataVolumeTemplate = this.parseVolumeClaimTemplate(R, dataVolumeName);
disks.push(_disk);
volumes.push(_volume);
diskNameLables.push(dataVolumeName);
if (R.source !== SOURCE_TYPE.CONTAINER) {
const prefixName = this.value.metadata?.name || '';
const dataVolumeName = this.parseDataVolumeName(R, prefixName);
const [needVolume, needVolumeClaimTemplate] = this.needVolumeRelatedInfo(R);
if (needVolume) {
const _volume = this.parseVolume(R, dataVolumeName);
volumes.push(_volume);
}
if (needVolumeClaimTemplate) {
const _dataVolumeTemplate = this.parseVolumeClaimTemplate(R, dataVolumeName);
volumeClaimTemplates.push(_dataVolumeTemplate);
}
});
@@ -765,6 +780,9 @@ export default {
}
}
const specDisks = this.spec?.template?.spec?.domain?.devices?.disks;
const mergedDisks = this.mergeDeviceList(specDisks, disks);
let spec = {
...this.spec,
runStrategy: this.runStrategy,
@@ -787,7 +805,7 @@ export default {
...this.spec.template?.spec?.domain,
devices: {
...this.spec.template?.spec?.domain?.devices,
disks,
disks: mergedDisks,
},
},
volumes,
@@ -886,13 +904,16 @@ export default {
interfaces.push(_interface);
});
const specInterfaces = this.spec?.template?.spec?.domain?.devices?.interfaces;
const mergedInterfaces = this.mergeInterfaceList(specInterfaces, interfaces);
const spec = {
...this.spec.template.spec,
domain: {
...this.spec.template.spec.domain,
devices: {
...this.spec.template.spec.domain.devices,
interfaces,
interfaces: mergedInterfaces,
},
},
networks
@@ -1005,6 +1026,20 @@ export default {
this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
},
parseDataVolumeName(R, prefixName) {
let dataVolumeName = '';
if (R.source === SOURCE_TYPE.ATTACH_VOLUME) {
dataVolumeName = R.volumeName;
} else if (this.isClone || !this.hasCreateVolumes.includes(R.realName)) {
dataVolumeName = `${ prefixName }-${ R.name }-${ randomStr(5).toLowerCase() }`;
} else {
dataVolumeName = R.realName;
}
return dataVolumeName;
},
parseDisk(R, index) {
const out = { name: R.name };
@@ -1035,7 +1070,9 @@ export default {
},
parseVolumeClaimTemplate(R, dataVolumeName) {
if (!String(R.size).includes('Gi') && R.size) {
const sizeString = String(R.size);
if (!(sizeString.includes('Gi') || sizeString.includes('Ti')) && R.size) {
R.size = `${ R.size }${ GIBIBYTE }`;
}
@@ -1526,6 +1563,49 @@ export default {
this.refreshYamlEditor();
},
mergeInterfaceList(specInterfaceList, interfaceList) {
if (!specInterfaceList || specInterfaceList.length === 0) {
return interfaceList;
}
const specInterfaceMap = new Map(specInterfaceList.map((iface) => [iface.name, iface]));
return interfaceList.map((iface) => {
const specInterface = specInterfaceMap.get(iface.name);
if (specInterface) {
const merged = { ...specInterface, ...iface };
// currently we only have bridge and masquerade network type, they are mutually exclusive
if (iface['bridge']) {
delete merged['masquerade'];
} else {
delete merged['bridge'];
}
return merged;
}
return iface;
});
},
mergeDeviceList(specDeviceList, deviceList) {
if (!specDeviceList || specDeviceList.length === 0) {
return deviceList;
}
const specDeviceMap = new Map(specDeviceList.map((device) => [device.name, device]));
return deviceList.map((device) => {
const specDevice = specDeviceMap.get(device.name);
if (specDevice) {
return { ...specDevice, ...device };
}
return device;
});
},
refreshYamlEditor() {
this.$nextTick(() => {
this.$refs.yamlEditor?.updateValue();
@@ -1585,7 +1665,8 @@ export default {
secretRef: {
handler(secret) {
if (secret && this.resourceType !== HCI.BACKUP) {
// we should not inherit the secret if it's from VM template.
if (secret && this.resourceType !== HCI.BACKUP && !this.useTemplate) {
this.secretName = secret?.metadata.name;
}
},
@@ -0,0 +1,116 @@
import { escapeHtml } from '@shell/utils/string';
import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
import HarvesterResource from './harvester';
/**
* Class representing vGPU MIGConfiguration resource.
* @extends HarvesterResource
*/
export default class MIGCONFIGURATION extends HarvesterResource {
get _availableActions() {
let out = super._availableActions;
out = out.map((action) => {
if (action.action === 'showConfiguration') {
return { ...action, enabled: !this.spec.enabled };
} else if (action.action === 'goToEditYaml') {
return { ...action, enabled: !this.spec.enabled };
} else if (action.action === 'goToEdit') {
// need to wait for status to be disabled or empty value, then allow user to editConfig
return { ...action, enabled: !this.spec.enabled && ['disabled', ''].includes(this.configStatus) };
} else {
return action;
}
});
out.push(
{
action: 'enableConfig',
enabled: !this.isEnabled,
icon: 'icon icon-fw icon-dot',
label: 'Enable',
},
{
action: 'disableConfig',
enabled: this.isEnabled,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable',
},
);
return out;
}
get canYaml() {
return false;
}
get disableResourceDetailDrawer() {
return true;
}
get canDelete() {
return false;
}
get configStatus() {
return this.status.status;
}
get actualState() {
return this.isEnabled ? 'Enabled' : 'Disabled';
}
get stateDisplay() {
return this.actualState;
}
get stateColor() {
const state = this.actualState;
return colorForState(state);
}
get isEnabled() {
return this.spec.enabled;
}
get configuredProfiles() {
const configuredProfiles = this.spec?.profileSpec?.filter((p) => p.requested > 0) || [];
if (configuredProfiles.length === 0) {
return '';
}
return configuredProfiles
.map((profile) => `${ profile.name } * ${ profile.requested }`)
.join(', ');
}
async enableConfig() {
try {
this.spec.enabled = true;
await this.save();
} catch (err) {
this.$dispatch('growl/fromError', {
title: this.t('generic.notification.title.error', { name: escapeHtml(this.name) }),
err,
}, { root: true });
}
}
async disableConfig() {
const { enabled: currentEnabled } = this.spec;
try {
this.spec.enabled = false;
await this.save();
} catch (err) {
this.spec.enabled = currentEnabled;
this.$dispatch('growl/fromError', {
title: this.t('generic.notification.title.error', { name: escapeHtml(this.name) }),
err,
}, { root: true });
}
}
}
@@ -1,6 +1,7 @@
import SteveModel from '@shell/plugins/steve/steve-class';
import { escapeHtml } from '@shell/utils/string';
import { HCI } from '../types';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
const STATUS_DISPLAY = {
enabled: {
@@ -32,7 +33,7 @@ export default class PCIDevice extends SteveModel {
out.push(
{
action: 'enablePassthroughBulk',
enabled: !this.isEnabling,
enabled: !this.isEnabling && !this.isvGPUDevice,
icon: 'icon icon-fw icon-dot',
label: 'Enable Passthrough',
bulkable: true,
@@ -41,7 +42,7 @@ export default class PCIDevice extends SteveModel {
},
{
action: 'disablePassthrough',
enabled: this.isEnabling && this.claimedByMe,
enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable Passthrough',
bulkable: true,
@@ -52,6 +53,14 @@ export default class PCIDevice extends SteveModel {
return out;
}
get isvGPUDevice() {
if (!this.vGPUAsPCIDeviceFeatureEnabled) {
return false;
}
return !!this.metadata?.labels?.[HCI_ANNOTATIONS.PARENT_SRIOV_GPU];
}
get canYaml() {
return false;
}
@@ -144,6 +153,12 @@ export default class PCIDevice extends SteveModel {
// 'disable' passthrough deletes claim
// backend should return error if device is in use
async disablePassthrough() {
if (!this.allowDisable) {
this.showDetachWarning();
return;
}
try {
if (!this.claimedByMe) {
throw new Error(this.$rootGetters['i18n/t']('harvester.pci.cantUnclaim', { name: escapeHtml(this.metadata.name) }));
@@ -169,4 +184,24 @@ export default class PCIDevice extends SteveModel {
get groupByDevice() {
return this.status?.description;
}
get vGPUAsPCIDeviceFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
}
showDetachWarning() {
this.$dispatch('growl/warning', {
title: this.$rootGetters['i18n/t']('harvester.pci.detachWarning.title'),
message: this.$rootGetters['i18n/t']('harvester.pci.detachWarning.message'),
timeout: 5000
}, { root: true });
}
get allowDisable() {
return this._allowDisable;
}
set allowDisable(value) {
this._allowDisable = value;
}
}

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