Compare commits

...

60 Commits

Author SHA1 Message Date
Harvester Bot
ff692affb0
chore: bump version to 1.7.2-rc1 (#856)
Signed-off-by: Harvester Bot <94133267+harvesterhci-io-github-bot@users.noreply.github.com>
2026-05-07 00:14:54 +08:00
mergify[bot]
cfb3392e2e
feat: add Insecure Skip TLS Verify checkbox in cluster-registration-url setting (backport #716) (#838)
* 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>
(cherry picked from commit 62b80b3cec62c608d51b98e7a91a0146a91e9791)

# Conflicts:
#	pkg/harvester/config/feature-flags.js

* chore: resolve backport merge conflict

Signed-off-by: Ivan Sim <ivan.sim@suse.com>

---------

Signed-off-by: Ivan Sim <ivan.sim@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Ivan Sim <ivan.sim@suse.com>
2026-04-29 12:12:35 +08:00
renovate[bot]
fe42708361
deps: update patch dependencies (#831)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-27 11:58:53 +08:00
renovate[bot]
62df6f5cbb
deps: update minor dependencies (#832)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-27 11:55:09 +08:00
renovate[bot]
2da7d0eb58
deps: update fossas/fossa-action action to v1.9.0 (#806)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 14:14:02 +08:00
mergify[bot]
251d92afba
fix: the rancher-eio/read-vault-secret sha (#786) (#803)
(cherry picked from commit 4ce35ce075e7a1fd77fd82e9f20e932c05565216)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-04-07 16:13:15 +08:00
mergify[bot]
7f3865343d
ci: add add_minrelaseday to delay dep update (#798) (#799)
(cherry picked from commit 6dd9b333365f172bf7a9993a9e3a82c9d49b27ad)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-04-07 16:08:59 +08:00
mergify[bot]
8f7a153665
ci: remove the single quota for use commit hash (#767) (#802)
(cherry picked from commit 8083a41df04ed5c82346cf3798dd85eedcb1e13a)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-04-07 16:05:23 +08:00
mergify[bot]
2a376933f2
chore: pin GH Actions to commit sha (#765) (#801)
(cherry picked from commit 62801b3b1371a221f0c485abe50f22b005155fe7)

# Conflicts:
#	.github/workflows/fossa.yml

Co-authored-by: freeze <1615081+Vicente-Cheng@users.noreply.github.com>
2026-04-07 16:02:32 +08:00
renovate[bot]
e5c253f044
deps: update dependency yaml to v2.8.3 (#756)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-22 13:05:42 +00:00
renovate[bot]
f0a9fe76a1
deps: update dependency @types/node to v20.19.37 (#728)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-08 06:15:00 +00:00
mergify[bot]
a0a797d5b9
fix: missing Download Logs button in Rancher integration mode (#715) (#722)
* fix: missing Download Logs button in Rancher integration mode



* refactor: add comment



---------


(cherry picked from commit de103ff91e71be1670f80bf24f9873bd7330c040)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-03-03 00:28:25 +08:00
renovate[bot]
1cc6f34d84
deps: update patch digest dependencies (#720)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-01 21:07:22 +00:00
renovate[bot]
ebfc4e8369
deps: update dependency qs to v6.15.0 (#711)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 12:04:34 +08:00
renovate[bot]
bd647738ae
deps: update patch digest dependencies (#706)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-08 09:11:33 +00:00
Andy Lee
c6fb969d7e
chore: update version v1.7.1 (#704)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-02-06 14:48:07 +08:00
Andy Lee
2afb04947d
chore: update v1.7.1-rc4 version
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-02-03 16:42:06 +08:00
Andy Lee
730c68bf14
chore: add v1.7.1 feature flag (#701)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-30 15:11:08 +08:00
freeze
20bee39a6c
chore: bump to v1.7.1-rc3 (#699)
Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
2026-01-29 01:10:39 +08:00
Andy Lee
dbb199d7bb
chore: bump to v1.7.1-rc2
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-27 22:37:48 +08:00
mergify[bot]
5488979448
fix: use longhorn-static for upgrade vmimage (#690) (#692)
(cherry picked from commit 0647600e88b59ccdf6a8d8f78d88b972cd604185)

Signed-off-by: Cooper Tseng <cooper.tseng@suse.com>
2026-01-22 09:51:01 +08:00
mergify[bot]
9e588e90c2
fix: remove isCordoned condition (#689) (#691)
(cherry picked from commit 99dbba7958c5bffb38aa68a3c6f6f0f44706ebae)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-01-21 17:43:08 +08:00
Andy Lee
9378277102
chore: change to v1.7.1-rc1 version
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-20 17:34:15 +08:00
mergify[bot]
b5e78018a5
fix: use file as field name instead of chunk in cdi vmimage upload (#684) (#688)
(cherry picked from commit 915559962a91802789750cec7549b61baea096e7)

Signed-off-by: Cooper Tseng <cooper.tseng@suse.com>
Co-authored-by: Kuan-Po Tseng <brandboat@gmail.com>
2026-01-20 11:00:50 +08:00
renovate[bot]
f411a0c0af
deps: update patch digest dependencies (#687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-18 08:48:10 +00:00
mergify[bot]
cfa58985cf
fix: do not set cpu.maxSockets on UI (#674) (#685)
* fix: do not set cpu.maxSockets for ARM clusters



* fix: remove maxSocket to fix bug on ARM cluster



---------


(cherry picked from commit b1b1a31c04a2f0b20fdfee42f987c694614617bf)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-01-16 17:15:34 +08:00
renovate[bot]
66a8f9d0e7
deps: update patch digest dependencies (#680)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-11 08:53:44 +00:00
freeze
bf61c7dd7d
chore: bump version to v1.7.1-dev (#673)
Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
2026-01-04 23:21:46 +08:00
mergify[bot]
56d97260c4
fix: drop mac-address annotation from vm template to prevent MAC address reusing (#663) (#669)
related-to: harvester/harvester#9789

(cherry picked from commit 1352246e1efdab691b7ccba9e1d02da01b6844a1)

Signed-off-by: Tim Liou <tim.liou@suse.com>
Co-authored-by: Tim Liou <tim.liou@suse.com>
2026-01-02 18:13:35 +08:00
mergify[bot]
beabb34920
docs: add README.md in pkg/harvester (#661) (#662)
(cherry picked from commit fe3a12e28ca6b28193b18e665af151078ce46499)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-01-02 16:18:06 +08:00
renovate[bot]
d2609157bd
deps: update dependency qs to v6.14.1 [security] (#668)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-02 00:42:31 +00:00
mergify[bot]
8fbe1943d8
chore: bump version to v1.7.0 (#658) (#659)
(cherry picked from commit a86302c9d5f2c1e065fb2cfe9cfd8d927bdc239b)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-12-22 13:41:18 +08:00
mergify[bot]
ec6bc4d639
chore: bump version to v1.7.0-rc7 (#656) (#657)
(cherry picked from commit 5fe7e13fcd6e46f32573e97a8d7bd8710078488e)

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
Co-authored-by: freeze <1615081+Vicente-Cheng@users.noreply.github.com>
2025-12-16 20:27:51 +08:00
renovate[bot]
3824a14730
deps: update dependency @types/node to v20.19.27 (#655)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-14 08:59:31 +00:00
mergify[bot]
0fc8bece02
chore: bump version to v1.7.0-rc6 (#649) (#650)
(cherry picked from commit 57695886336a7553b5ffdf6bb65093fa2037b3e6)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-12-11 16:50:51 +08:00
mergify[bot]
39764af627
fix: failed to create multiple VMs (#647) (#648)
(cherry picked from commit b29950f99cbcaf40919654fee8f6a58201a33574)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-12-11 16:17:02 +08:00
mergify[bot]
bdc87bda0e
fix: do not inherit template secret when creating new VM (#643) (#646)
(cherry picked from commit 6c27a462748575da1fd6e0f04baf116063f7498f)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-12-10 17:18:33 +08:00
mergify[bot]
e0dc77624b
feat: read addon displayname from label and add descheduler description (#644) (#645)
* refactor: display addon name from addon.harvesterhci.io/displayName label



* refactor: add descheduler description



---------


(cherry picked from commit b03fffbc3014dc7214177cac69bfcecdf7cb30c3)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-12-10 15:16:08 +08:00
renovate[bot]
c3ba10bd22
deps: update dependency node-forge to v1.3.3 (#641)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-07 09:15:44 +00:00
mergify[bot]
5b7d54d0a3
chore: bump to v1.7.0-rc5 (#636) (#637)
(cherry picked from commit 416098ffd822ff531d8cfa42fdbfed1c8d53ff53)

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
Co-authored-by: freeze <1615081+Vicente-Cheng@users.noreply.github.com>
2025-12-04 22:45:15 +08:00
renovate[bot]
99a216dfa0
deps: update dependency yaml to v2.8.2 (#634)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-30 16:55:46 +00:00
mergify[bot]
e4c85f510e
chore: bump to v1.7.0-rc4 (#621) (#629)
(cherry picked from commit 3d7b96d86d3fce8f7746f1eca1a90ea9b31bb67d)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-28 13:23:55 +08:00
mergify[bot]
f391f018de
fix: create new secret on vm creation (#614) (#628)
* fix: create new secret on vm creation



* fix: ensure parseVM result is immutable



---------


(cherry picked from commit 0b37467f7637639209c27570bfe5633a41b96ac0)

Signed-off-by: Caio Torres <caio.torres@suse.com>
Co-authored-by: Caio Torres <caio.torres@suse.com>
2025-11-27 17:42:46 +08:00
renovate[bot]
4b2e92ea15
deps: update dependency node-forge to v1.3.2 [security] (#627)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-27 14:31:50 +08:00
mergify[bot]
2f956d5946
feat: allow user to attach volume to muliple VMs (#620) (#622)
(cherry picked from commit d94003f8c28876b4e5803bb04a6049ab63f812e6)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-27 14:30:44 +08:00
renovate[bot]
5f8d556ea2
deps: update patch digest dependencies (#619)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-23 09:32:21 +00:00
mergify[bot]
396ab48f1c
chore: bump version to v1.7.0-rc3 (#612) (#613) 2025-11-20 10:25:03 +08:00
mergify[bot]
6b8c079018
fix: condition render namespaceOptions (#607) (#611)
(cherry picked from commit 1b183febdc5e13d29d48b655198114f7d38af526)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-11-19 17:40:03 +08:00
mergify[bot]
7f638e86c8
fix: change migConfiguration model to inherit from harvester resource (#608) (#609)
(cherry picked from commit 70d3b656f78439ba56816d75b526bc5637a67e1f)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-19 17:35:52 +08:00
mergify[bot]
0c4955a766
feat: create related image storageclass before OS upgrade (#595) (#606)
* feat: create related image SC before upgrade



* refactor: update spec.targetStorageClassName



* refactor: based on comment



---------


(cherry picked from commit 10d19cd329cce7e376ce2712a8843742d8968b65)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-17 17:34:51 +08:00
mergify[bot]
3f4ff30275
feat: modified placeholder (#599) (#605)
(cherry picked from commit 1715ae754caf4cb6e0688d8eb4326f9b0a90f960)

Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-11-17 17:08:20 +08:00
renovate[bot]
8e0332a364
deps: update dependency @types/node to v20.19.25 (#604)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-16 13:03:17 +00:00
mergify[bot]
6700b2055e
feat: add support for configuring transparent hugepages (#414) (#598)
* feat: add support for configuring transparent hugepages

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



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

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




---------




(cherry picked from commit 30de2b1a185ccc2a3ec159e220de742dd2156229)

Signed-off-by: Tim Serong <tserong@suse.com>
Co-authored-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 14:15:27 +08:00
mergify[bot]
e486852f7a
chore: bump version to v1.7.0-rc2 (#596) (#597)
(cherry picked from commit 6fedcc353c59df9e36693822f56c0be78029a46a)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-12 09:21:01 +08:00
mergify[bot]
c19341bec9
feat: support for HotPlugNICs from Kubevirt (#582) (#594)
* 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

---------


(cherry picked from commit f9bff21e840885a120679864a0ef312163bd48a7)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-11-11 11:46:43 +08:00
mergify[bot]
8cbb9d6b18
chore: update yarn.lock for @rancher/shell v3.0.8-rc.8 (#591) (#592)
(cherry picked from commit 6735826e15d6515f87fa752418141ebb79cc5c1f)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-10 16:19:48 +08:00
mergify[bot]
00f0953592
chore: bump shell version to v3.0.8-rc.8 (#588) (#590)
(cherry picked from commit 9e17e239cf9d6e65f5d2f42636bac19cef70bb16)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-10 15:05:04 +08:00
mergify[bot]
58507f0b2e
ci: lint last commit if is empty string or all zero (#584) (#586)
(cherry picked from commit db58024351e06c6ddc90d6143cae9133ddbbfedc)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-06 15:53:50 +08:00
mergify[bot]
7785d7f469
feat: enable snapshot and clone for LHv2 (#379) (#587)
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


(cherry picked from commit a1cf41bda92ceb399be21904ec267311f9568bdb)

Signed-off-by: Tim Serong <tserong@suse.com>
Co-authored-by: Tim Serong <tserong@suse.com>
2025-11-06 15:53:20 +08:00
mergify[bot]
2c043e0a8e
chore: bump version to v1.7.0-rc1 (#583) (#585)
(cherry picked from commit 81bf19419c56dbd670d4cf8a1b9b658bbae6ea4f)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-04 16:12:14 +08:00
51 changed files with 2337 additions and 864 deletions

View File

@ -4,7 +4,7 @@ description: Setup node and install dependencies
runs: runs:
using: 'composite' using: 'composite'
steps: steps:
- uses: actions/setup-node@v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'

View File

@ -33,12 +33,14 @@
{ {
"matchUpdateTypes": ["minor"], "matchUpdateTypes": ["minor"],
"groupName": "minor dependencies", "groupName": "minor dependencies",
"minimumReleaseAge": "7 days",
"labels": ["minor-update"], "labels": ["minor-update"],
"reviewers": ["a110605", "houhoucoop"] "reviewers": ["a110605", "houhoucoop"]
}, },
{ {
"matchUpdateTypes": ["patch", "digest"], "matchUpdateTypes": ["patch", "digest"],
"automerge": true, "automerge": true,
"minimumReleaseAge": "7 days",
"groupName": "patch digest dependencies", "groupName": "patch digest dependencies",
"labels": ["patch-update", "automerge"] "labels": ["patch-update", "automerge"]
} }

View File

@ -12,6 +12,6 @@ jobs:
if: github.event.pull_request.draft == false if: github.event.pull_request.draft == false
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: rancher/gh-issue-mgr/auto-assign-action@main - uses: rancher/gh-issue-mgr/auto-assign-action@b70f0bdf12a03e5e3f33e4f92ccb6c89deb3ebd9 # main
with: with:
configuration-path: .github/auto-assign-config.yaml configuration-path: .github/auto-assign-config.yaml

View File

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with: with:
ref: ${{ github.base_ref }} ref: ${{ github.base_ref }}

View File

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with: with:
ref: ${{ github.base_ref }} ref: ${{ github.base_ref }}

View File

@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Check package version - name: Check package version
env: env:

View File

@ -25,12 +25,12 @@ jobs:
name: Build & Upload Hosted name: Build & Upload Hosted
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
with: with:
fetch-depth: 1 fetch-depth: 1
# Note - Cannot use the setup action here as it uses a different yarn install arg # 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: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
@ -45,19 +45,20 @@ jobs:
run: ./scripts/build-upload-gate run: ./scripts/build-upload-gate
- name: Get gcs auth - name: Get gcs auth
uses: rancher-eio/read-vault-secrets@main uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
with: with:
secrets: | secrets: |
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ; secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
- name: Apply gcs auth - name: Apply gcs auth
# https://github.com/google-github-actions/auth # https://github.com/google-github-actions/auth
uses: 'google-github-actions/auth@v2' uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
with: with:
credentials_json: "${{ env.GOOGLE_AUTH }}" credentials_json: "${{ env.GOOGLE_AUTH }}"
- name: Upload build - 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 # https://github.com/google-github-actions/upload-cloud-storage
with: with:
path: ${{steps.build-hosted.outputs.BUILD_HOSTED_DIR}} path: ${{steps.build-hosted.outputs.BUILD_HOSTED_DIR}}
@ -71,12 +72,12 @@ jobs:
name: Build & Upload Embedded name: Build & Upload Embedded
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
with: with:
fetch-depth: 1 fetch-depth: 1
# Note - Cannot use the setup action here as it uses a different yarn install arg # 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: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
@ -89,19 +90,19 @@ jobs:
DISABLED_EMBED_PKG: https://releases.rancher.com/harvester-ui/plugin/harvester-1.0.3.tar.gz DISABLED_EMBED_PKG: https://releases.rancher.com/harvester-ui/plugin/harvester-1.0.3.tar.gz
- name: Get gcs auth - name: Get gcs auth
uses: rancher-eio/read-vault-secrets@main uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
with: with:
secrets: | secrets: |
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ; secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
- name: Apply gcs auth - name: Apply gcs auth
# https://github.com/google-github-actions/auth # https://github.com/google-github-actions/auth
uses: 'google-github-actions/auth@v2' uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
with: with:
credentials_json: "${{ env.GOOGLE_AUTH }}" credentials_json: "${{ env.GOOGLE_AUTH }}"
- name: Upload tar - name: Upload tar
uses: 'google-github-actions/upload-cloud-storage@v2' uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
with: with:
path: ${{steps.build-embedded.outputs.BUILD_EMBEDED_TGZ}} path: ${{steps.build-embedded.outputs.BUILD_EMBEDED_TGZ}}
destination: releases.rancher.com/harvester-ui/dashboard destination: releases.rancher.com/harvester-ui/dashboard
@ -114,12 +115,12 @@ jobs:
name: Build & Upload Plugin name: Build & Upload Plugin
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
with: with:
fetch-depth: 1 fetch-depth: 1
# Note - Cannot use the setup action here as it uses a different yarn install arg # 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: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
@ -133,19 +134,19 @@ jobs:
run: ./scripts/build-upload-gate run: ./scripts/build-upload-gate
- name: Get gcs auth - name: Get gcs auth
uses: rancher-eio/read-vault-secrets@main uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
with: with:
secrets: | secrets: |
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ; secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
- name: Apply gcs auth - name: Apply gcs auth
# https://github.com/google-github-actions/auth # https://github.com/google-github-actions/auth
uses: 'google-github-actions/auth@v2' uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
with: with:
credentials_json: "${{ env.GOOGLE_AUTH }}" credentials_json: "${{ env.GOOGLE_AUTH }}"
- name: Upload plugin tar - name: Upload plugin tar
uses: 'google-github-actions/upload-cloud-storage@v2' uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
with: with:
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_TARBALL}} path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_TARBALL}}
destination: releases.rancher.com/harvester-ui/plugin destination: releases.rancher.com/harvester-ui/plugin
@ -155,7 +156,7 @@ jobs:
process_gcloudignore: false process_gcloudignore: false
- name: Upload plugin directory - name: Upload plugin directory
uses: 'google-github-actions/upload-cloud-storage@v2' uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
with: with:
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_NAME}} path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_NAME}}
destination: releases.rancher.com/harvester-ui/plugin/${{steps.ci-build-pkg.outputs.PKG_NAME}} destination: releases.rancher.com/harvester-ui/plugin/${{steps.ci-build-pkg.outputs.PKG_NAME}}

View File

@ -27,14 +27,14 @@ jobs:
build-status: ${{ job.status }} build-status: ${{ job.status }}
steps: steps:
- name: Read Secrets - name: Read Secrets
uses: rancher-eio/read-vault-secrets@main uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
with: with:
secrets: | secrets: |
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | DOCKER_USERNAME ; secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | DOCKER_USERNAME ;
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD ; secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD ;
- name: Checkout repository (normal flow) - name: Checkout repository (normal flow)
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Enable Corepack - name: Enable Corepack
run: corepack enable run: corepack enable
@ -45,18 +45,18 @@ jobs:
git config user.email 'github-actions[bot]@users.noreply.github.com' git config user.email 'github-actions[bot]@users.noreply.github.com'
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with: with:
username: ${{ env.DOCKER_USERNAME }} username: ${{ env.DOCKER_USERNAME }}
password: ${{ env.DOCKER_PASSWORD }} password: ${{ env.DOCKER_PASSWORD }}
- name: Setup Helm - name: Setup Helm
uses: azure/setup-helm@v3 uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
with: with:
version: v3.8.0 version: v3.8.0
- name: Setup Nodejs with yarn caching - name: Setup Nodejs with yarn caching
uses: actions/setup-node@v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: '20' node-version: '20'
cache: yarn cache: yarn
@ -67,4 +67,4 @@ jobs:
- name: Build and push UI image - name: Build and push UI image
run: | run: |
publish="yarn publish-pkgs -cp -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}" publish="yarn publish-pkgs -cp -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
$publish $publish

View File

@ -13,7 +13,7 @@ jobs:
target_branch: ${{ steps.get-version.outputs.target_branch }} target_branch: ${{ steps.get-version.outputs.target_branch }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Determine target branch - name: Determine target branch
id: get-version id: get-version
@ -44,7 +44,7 @@ jobs:
version: ${{ steps.get_version.outputs.version }} version: ${{ steps.get_version.outputs.version }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Extract version from package.json - name: Extract version from package.json
id: get_version id: get_version
@ -62,7 +62,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup environment - name: Setup environment
run: | run: |
@ -70,7 +70,7 @@ jobs:
yarn install --frozen-lockfile yarn install --frozen-lockfile
- name: Setup Helm - name: Setup Helm
uses: azure/setup-helm@v3 uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
with: with:
version: v3.8.0 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 }} 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 - name: Upload charts artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with: with:
name: charts name: charts
path: tmp path: tmp
@ -94,7 +94,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout release branch - name: Checkout release branch
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with: with:
ref: '${{ github.ref_name }}' ref: '${{ github.ref_name }}'
@ -105,7 +105,7 @@ jobs:
echo "LAST_COMMIT=${LAST_COMMIT}" >> $GITHUB_ENV echo "LAST_COMMIT=${LAST_COMMIT}" >> $GITHUB_ENV
- name: Checkout target branch - name: Checkout target branch
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with: with:
ref: '${{ needs.setup-target-branch.outputs.target_branch }}' ref: '${{ needs.setup-target-branch.outputs.target_branch }}'
@ -121,7 +121,7 @@ jobs:
git config user.email 'github-actions[bot]@users.noreply.github.com' git config user.email 'github-actions[bot]@users.noreply.github.com'
- name: Download build artifacts - name: Download build artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with: with:
name: charts name: charts
@ -132,7 +132,7 @@ jobs:
git push origin ${{ needs.setup-target-branch.outputs.target_branch }} git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
- name: Run Helm chart releaser - name: Run Helm chart releaser
uses: helm/chart-releaser-action@v1.7.0 uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0
with: with:
charts_dir: ./charts charts_dir: ./charts
env: env:

View File

@ -17,7 +17,7 @@ jobs:
release_tag: ${{ steps.determine_tag.outputs.release_tag }} release_tag: ${{ steps.determine_tag.outputs.release_tag }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Determine release tag - name: Determine release tag
id: determine_tag id: determine_tag
@ -33,7 +33,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Check package version - name: Check package version
env: env:
TAG_VERSION: ${{ github.event.release.tag_name }} TAG_VERSION: ${{ github.event.release.tag_name }}
@ -43,7 +43,7 @@ jobs:
needs: needs:
- setup-release-tag - setup-release-tag
- check-version - 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: permissions:
actions: write actions: write
contents: write contents: write

View File

@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Check package version - name: Check package version
env: env:
TAG_VERSION: ${{github.ref_name}} TAG_VERSION: ${{github.ref_name}}

34
.github/workflows/fossa.yml vendored Normal file
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@d266f55186f80a893839f6e15662e67388e443e6 # v3
with:
secrets: |
secret/data/github/org/harvester/fossa/credentials token | FOSSA_API_KEY_PUSH_ONLY
- name: FOSSA scan
uses: fossas/fossa-action@ff70fe9fe17cbd2040648f1c45e8ec4e4884dcf3 # v1.9.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

View File

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with: with:
ref: ${{ github.base_ref }} ref: ${{ github.base_ref }}
- name: Setup Nodejs and yarn install - name: Setup Nodejs and yarn install

View File

@ -16,7 +16,7 @@ jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with: with:
fetch-depth: 0 # Need full history for commit-lint fetch-depth: 0 # Need full history for commit-lint
@ -41,8 +41,14 @@ jobs:
FROM="$GITHUB_BASE_SHA" FROM="$GITHUB_BASE_SHA"
TO="$GITHUB_HEAD_SHA" TO="$GITHUB_HEAD_SHA"
elif [ -n "$GITHUB_BEFORE" ] && [ -n "$GITHUB_AFTER" ]; then elif [ -n "$GITHUB_BEFORE" ] && [ -n "$GITHUB_AFTER" ]; then
FROM="$GITHUB_BEFORE" if [ "$GITHUB_BEFORE" = "0000000000000000000000000000000000000000" ]; then
TO="$GITHUB_AFTER" # first push to HEAD
FROM=""
TO="$GITHUB_AFTER"
else
FROM="$GITHUB_BEFORE"
TO="$GITHUB_AFTER"
fi
else else
echo "No valid commit range found, skipping commitlint." echo "No valid commit range found, skipping commitlint."
exit 0 exit 0
@ -51,7 +57,14 @@ jobs:
echo "FROM=$FROM" echo "FROM=$FROM"
echo "TO=$TO" 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: env:
GITHUB_EVENT_NAME: ${{ github.event_name }} GITHUB_EVENT_NAME: ${{ github.event_name }}
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }} GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}

View File

@ -1,13 +1,13 @@
{ {
"name": "harvester-ui-extension", "name": "harvester-ui-extension",
"version": "1.7.0-dev", "version": "1.7.2-rc1",
"private": false, "private": false,
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
}, },
"dependencies": { "dependencies": {
"@babel/plugin-transform-class-static-block": "7.28.3", "@babel/plugin-transform-class-static-block": "7.28.6",
"@rancher/shell": "3.0.5-rc.8", "@rancher/shell": "3.0.8-rc.8",
"cache-loader": "^4.1.0", "cache-loader": "^4.1.0",
"color": "4.2.3", "color": "4.2.3",
"ip": "2.0.1", "ip": "2.0.1",
@ -20,17 +20,17 @@
"cronstrue": "2.59.0", "cronstrue": "2.59.0",
"d3-color": "3.1.0", "d3-color": "3.1.0",
"ejs": "3.1.10", "ejs": "3.1.10",
"follow-redirects": "1.15.11", "follow-redirects": "1.16.0",
"glob": "7.2.3", "glob": "7.2.3",
"glob-parent": "6.0.2", "glob-parent": "6.0.2",
"json5": "2.2.3", "json5": "2.2.3",
"@types/lodash": "4.17.20", "@types/lodash": "4.17.24",
"merge": "2.1.1", "merge": "2.1.1",
"node-forge": "1.3.1", "node-forge": "1.4.0",
"nth-check": "2.1.1", "nth-check": "2.1.1",
"qs": "6.14.0", "qs": "6.15.1",
"roarr": "7.21.2", "roarr": "7.21.4",
"semver": "7.7.3", "semver": "7.7.4",
"@vue/cli-service/html-webpack-plugin": "^5.0.0" "@vue/cli-service/html-webpack-plugin": "^5.0.0"
}, },
"scripts": { "scripts": {

178
pkg/harvester/README.md Normal file
View File

@ -0,0 +1,178 @@
# 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 detailed installation instructions, please refer to the [official Harvester documentation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210).
## 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-2025 [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.

View File

@ -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>

View File

@ -18,16 +18,20 @@ export default {
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE }); await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
try { if (this.customSupportBundleFeatureEnabled) {
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle'); try {
const response = await this.$store.dispatch('harvester/request', { url }); 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 || []; this.defaultNamespaces = response.data || [];
} catch (error) { } catch (error) {
this.defaultNamespaces = [];
}
} else {
this.defaultNamespaces = []; this.defaultNamespaces = [];
} finally {
this.loading = false;
} }
this.loading = false;
}, },
data() { data() {
@ -42,24 +46,38 @@ export default {
}, },
computed: { computed: {
customSupportBundleFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
},
allNamespaces() { allNamespaces() {
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id); return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
}, },
filteredNamespaces() { filteredNamespaces() {
if (!this.customSupportBundleFeatureEnabled) {
return this.allNamespaces;
}
const defaultIds = this.defaultNamespaces.map((ns) => ns.id); const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns)); return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
}, },
namespaceOptions() { namespaceOptions() {
const mappedNamespaces = this.filteredNamespaces.map((ns) => ({ label: ns, value: ns }));
if (!this.customSupportBundleFeatureEnabled) {
return mappedNamespaces;
}
const allSelected = const allSelected =
this.namespaces.length === this.filteredNamespaces.length && this.namespaces.length === this.filteredNamespaces.length &&
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns)); 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 }; 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];
} }
}, },

View File

@ -9,4 +9,5 @@ export const DOC = {
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`, SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
VPC_CONFIGURATION_EXAMPLES: `/networking/kubeovn-vpc#vpc-peering-configuration-examples`, VPC_CONFIGURATION_EXAMPLES: `/networking/kubeovn-vpc#vpc-peering-configuration-examples`,
NETWORK_POLICY: `/networking/kubeovn-vm-isolation/#network-policies`, NETWORK_POLICY: `/networking/kubeovn-vm-isolation/#network-policies`,
TRANSPARENT_HUGEPAGES: `https://docs.kernel.org/admin-guide/mm/transhuge.html`,
}; };

View File

@ -53,8 +53,13 @@ const FEATURE_FLAGS = {
'vmMachineTypeAuto', 'vmMachineTypeAuto',
'lhV2VolExpansion', 'lhV2VolExpansion',
'l2VlanTrunkMode', 'l2VlanTrunkMode',
'kubevirtMigration' 'kubevirtMigration',
] 'hotplugNic'
],
'v1.7.1': [],
'v1.7.2': [
'clusterRegistrationTLSVerify'
],
}; };
const generateFeatureFlags = () => { const generateFeatureFlags = () => {

View File

@ -50,6 +50,7 @@ export const HCI = {
STORAGE_CLASS: 'harvesterhci.io/storageClassName', STORAGE_CLASS: 'harvesterhci.io/storageClassName',
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io', STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental', ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
ADDON_DISPLAYNAME: 'addon.harvesterhci.io/displayName',
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error', VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine', VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm', KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
@ -76,4 +77,5 @@ export const HCI = {
CLONE_STRATEGY: 'cdi.harvesterhci.io/storageProfileCloneStrategy', CLONE_STRATEGY: 'cdi.harvesterhci.io/storageProfileCloneStrategy',
VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes', VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes',
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass', VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
MAC_ADDRESS: 'harvesterhci.io/mac-address',
}; };

View File

@ -123,7 +123,8 @@ export const HCI_ALLOWED_SETTINGS = {
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = { export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
[HCI_SETTING.CLUSTER_REGISTRATION_URL]: { [HCI_SETTING.CLUSTER_REGISTRATION_URL]: {
kind: 'url', kind: 'custom',
from: 'import',
canReset: true, canReset: true,
}, },
[HCI_SETTING.UI_PL]: { [HCI_SETTING.UI_PL]: {

View File

@ -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>

View File

@ -27,6 +27,7 @@ import Instance from './VirtualMachineInstance';
import Disk from './HarvesterHostDisk'; import Disk from './HarvesterHostDisk';
import VlanStatus from './VlanStatus'; import VlanStatus from './VlanStatus';
import HarvesterKsmtuned from './HarvesterKsmtuned.vue'; import HarvesterKsmtuned from './HarvesterKsmtuned.vue';
import HarvesterHugepages from './HarvesterHugepages.vue';
import HarvesterSeeder from './HarvesterSeeder'; import HarvesterSeeder from './HarvesterSeeder';
const LONGHORN_SYSTEM = 'longhorn-system'; const LONGHORN_SYSTEM = 'longhorn-system';
@ -46,6 +47,7 @@ export default {
VlanStatus, VlanStatus,
LabelValue, LabelValue,
HarvesterKsmtuned, HarvesterKsmtuned,
HarvesterHugepages,
Loading, Loading,
SortableTable, SortableTable,
HarvesterSeeder, HarvesterSeeder,
@ -209,6 +211,12 @@ export default {
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED); 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() { hasBlockDevicesSchema() {
return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE); return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE);
}, },
@ -468,6 +476,16 @@ export default {
/> />
</Tab> </Tab>
<Tab
v-if="hasHugepagesSchema"
name="hugepages"
:weight="0"
:show-header="false"
:label="t('harvester.host.tabs.hugepages')"
>
<HarvesterHugepages :node="value" />
</Tab>
<Tab <Tab
v-if="seederEnabled" v-if="seederEnabled"
name="seeder" name="seeder"

View File

@ -213,6 +213,7 @@ export default {
const diskRows = this.getDiskRows(neu); const diskRows = this.getDiskRows(neu);
this['diskRows'] = diskRows; this['diskRows'] = diskRows;
this['networkRows'] = this.getNetworkRows(neu, { fromTemplate: false, init: false });
}, },
deep: true deep: true
} }
@ -265,6 +266,7 @@ export default {
<Network <Network
v-model:value="networkRows" v-model:value="networkRows"
mode="view" mode="view"
:vm="value"
/> />
</Tab> </Tab>

View File

@ -32,7 +32,7 @@ export default {
resources: { resources: {
type: Array, type: Array,
required: true required: true
} },
}, },
data() { data() {
@ -43,7 +43,7 @@ export default {
...mapState('action-menu', ['modalData']), ...mapState('action-menu', ['modalData']),
title() { title() {
return this.modalData.title || 'dialog.promptRemove.title'; return this.modalData?.title || 'dialog.promptRemove.title';
}, },
formattedType() { formattedType() {
@ -51,7 +51,7 @@ export default {
}, },
warningMessage() { warningMessage() {
if (this.modalData.warningMessage) return this.modalData.warningMessage; if (this.modalData?.warningMessage) return this.modalData.warningMessage;
const isPlural = this.type.endsWith('s'); const isPlural = this.type.endsWith('s');
const thisOrThese = isPlural ? 'these' : 'this'; const thisOrThese = isPlural ? 'these' : 'this';
@ -145,6 +145,7 @@ export default {
try { try {
for (const resource of this.resources) { for (const resource of this.resources) {
await resource.remove(); await resource.remove();
if (this.modalData?.extraActionAfterRemove) await this.modalData.extraActionAfterRemove();
} }
buttonDone(true); buttonDone(true);
this.close(); this.close();

View File

@ -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>

View File

@ -11,7 +11,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
export default { export default {
name: 'HotplugModal', name: 'HotplugVolumeModal',
emits: ['close'], emits: ['close'],
@ -62,7 +62,7 @@ export default {
return false; return false;
} }
return !pvc.attachVM; return true;
}) })
.map((pvc) => { .map((pvc) => {
return { return {
@ -90,7 +90,7 @@ export default {
if (res._status === 200 || res._status === 204) { if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', { this.$store.dispatch('growl/success', {
title: this.t('generic.notification.title.succeed'), 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 }); }, { root: true });
this.close(); this.close();
@ -122,7 +122,7 @@ export default {
> >
<template #title> <template #title>
<h4 <h4
v-clean-html="t('harvester.modal.hotplug.title')" v-clean-html="t('harvester.modal.hotplugVolume.title')"
class="text-default-text" class="text-default-text"
/> />
</template> </template>

View File

@ -1,13 +1,12 @@
<script> <script>
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import { exceptionToErrorsArray } from '@shell/utils/error'; import { exceptionToErrorsArray } from '@shell/utils/error';
import { Card } from '@components/Card'; import { Card } from '@components/Card';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton'; import AsyncButton from '@shell/components/AsyncButton';
export default { export default {
name: 'HarvesterHotUnplugModal', name: 'HarvesterHotUnplug',
emits: ['close'], emits: ['close'],
@ -35,8 +34,25 @@ export default {
actionResource() { actionResource() {
return this.resources[0]; return this.resources[0];
}, },
diskName() {
return this.modalData.diskName; name() {
return this.modalData.name;
},
isVolume() {
return this.modalData.type === 'volume';
},
titleKey() {
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.title' : 'harvester.virtualMachine.hotUnplug.detachNIC.title';
},
actionLabelKey() {
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabel' : 'harvester.virtualMachine.hotUnplug.detachNIC.actionLabel';
},
successMessageKey() {
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.success' : 'harvester.virtualMachine.hotUnplug.detachNIC.success';
} }
}, },
@ -47,14 +63,20 @@ export default {
async save(buttonCb) { async save(buttonCb) {
try { try {
const res = await this.actionResource.doAction('removeVolume', { diskName: this.diskName }); let res;
if (this.isVolume) {
res = await this.actionResource.doAction('removeVolume', { diskName: this.name });
} else {
res = await this.actionResource.doAction('removeNic', { interfaceName: this.name });
}
if (res._status === 200 || res._status === 204) { if (res._status === 200 || res._status === 204) {
this.$store.dispatch( this.$store.dispatch(
'growl/success', 'growl/success',
{ {
title: this.t('generic.notification.title.succeed'), 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 } { root: true }
); );
@ -64,14 +86,14 @@ export default {
} else { } else {
const error = [res?.data] || exceptionToErrorsArray(res); const error = [res?.data] || exceptionToErrorsArray(res);
this['errors'] = error; this.errors = error;
buttonCb(false); buttonCb(false);
} }
} catch (err) { } catch (err) {
const error = err?.data || err; const error = err?.data || err;
const message = exceptionToErrorsArray(error); const message = exceptionToErrorsArray(error);
this['errors'] = message; this.errors = message;
buttonCb(false); buttonCb(false);
} }
} }
@ -87,7 +109,7 @@ export default {
> >
<template #title> <template #title>
<h4 <h4
v-clean-html="t('harvester.virtualMachine.unplug.title', { name: diskName })" v-clean-html="t(titleKey, { name })"
class="text-default-text" class="text-default-text"
/> />
<Banner <Banner
@ -111,9 +133,9 @@ export default {
<AsyncButton <AsyncButton
mode="apply" mode="apply"
:action-label="t('harvester.virtualMachine.unplug.actionLabel')" :action-label="t(actionLabelKey)"
:waiting-label="t('harvester.virtualMachine.unplug.actionLabel')" :waiting-label="t(actionLabelKey)"
:success-label="t('harvester.virtualMachine.unplug.actionLabel')" :success-label="t(actionLabelKey)"
@click="save" @click="save"
/> />
</div> </div>
@ -132,4 +154,8 @@ export default {
justify-content: flex-end; justify-content: flex-end;
width: 100%; width: 100%;
} }
::v-deep(.card-title) {
display: block;
}
</style> </style>

View File

@ -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>

View File

@ -28,6 +28,7 @@ import { HCI } from '../../types';
import HarvesterDisk from './HarvesterDisk'; import HarvesterDisk from './HarvesterDisk';
import HarvesterSeeder from './HarvesterSeeder'; import HarvesterSeeder from './HarvesterSeeder';
import HarvesterKsmtuned from './HarvesterKsmtuned'; import HarvesterKsmtuned from './HarvesterKsmtuned';
import HarvesterHugepages from './HarvesterHugepages';
import Tags from '../../components/DiskTags'; import Tags from '../../components/DiskTags';
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass'; import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
@ -50,6 +51,7 @@ export default {
ArrayListGrouped, ArrayListGrouped,
HarvesterDisk, HarvesterDisk,
HarvesterKsmtuned, HarvesterKsmtuned,
HarvesterHugepages,
ButtonDropdown, ButtonDropdown,
KeyValue, KeyValue,
Banner, Banner,
@ -225,6 +227,12 @@ export default {
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED); 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() { hasBlockDevicesSchema() {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
@ -647,6 +655,17 @@ export default {
</template> </template>
</ArrayListGrouped> </ArrayListGrouped>
</Tab> </Tab>
<Tab
v-if="hasHugepagesSchema"
name="Hugepages"
:weight="70"
:label="t('harvester.host.tabs.hugepages')"
>
<HarvesterHugepages
:node="value"
:register-before-hook="registerBeforeHook"
/>
</Tab>
<Tab <Tab
v-if="hasKsmtunedSchema" v-if="hasKsmtunedSchema"
name="Ksmtuned" name="Ksmtuned"

View File

@ -371,7 +371,7 @@ export default {
<div class="key"> <div class="key">
<input <input
v-model="scope.row.value" v-model="scope.row.value"
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')" :placeholder="t('harvester.subnet.excludeIPs.placeholder')"
/> />
</div> </div>
</template> </template>

View File

@ -1,6 +1,5 @@
<script> <script>
import InfoBox from '@shell/components/InfoBox'; import InfoBox from '@shell/components/InfoBox';
import { NETWORK_ATTACHMENT } from '@shell/config/types'; import { NETWORK_ATTACHMENT } from '@shell/config/types';
import { sortBy } from '@shell/utils/sort'; import { sortBy } from '@shell/utils/sort';
import { clone } from '@shell/utils/object'; import { clone } from '@shell/utils/object';
@ -14,6 +13,13 @@ export default {
components: { InfoBox, Base }, components: { InfoBox, Base },
props: { props: {
vm: {
type: Object,
default: () => {
return {};
}
},
mode: { mode: {
type: String, type: String,
default: 'create' default: 'create'
@ -32,10 +38,15 @@ export default {
} }
}, },
async fetch() {
await this.fetchHotunplugData();
},
data() { data() {
return { 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; return out;
}, },
canCheckHotunplug() {
return !!this.vm?.actions?.findHotunpluggableNics;
},
vmState() {
return this.vm?.stateDisplay;
}
}, },
watch: { watch: {
value(neu) { value(neu) {
this.rows = neu; this.rows = this.mergeHotplugData(clone(neu));
}, },
vmState(newState, oldState) {
if (newState !== oldState) {
this.fetchHotunplugData();
}
}
}, },
methods: { 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) { add(type) {
const name = this.generateName(); const name = this.generateName();
@ -118,7 +171,11 @@ export default {
update() { update() {
this.$emit('update:value', this.rows); this.$emit('update:value', this.rows);
} },
unplugNIC(network) {
this.vm.unplugNIC(network.name);
},
} }
}; };
</script> </script>
@ -129,17 +186,28 @@ export default {
v-for="(row, i) in rows" v-for="(row, i) in rows"
:key="i" :key="i"
> >
<button <div class="box-title mb-10">
v-if="!isView" <h3>
type="button" {{ t('harvester.virtualMachine.network.title') }}
class="role-link remove-vol" </h3>
@click="remove(row)" <button
> v-if="!isView"
<i class="icon icon-x" /> type="button"
</button> class="role-link btn btn-sm remove"
@click="remove(row)"
<h3> {{ t('harvester.virtualMachine.network.title') }} </h3> >
<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 <Base
v-model:value="rows[i]" v-model:value="rows[i]"
:rows="rows" :rows="rows"
@ -162,16 +230,13 @@ export default {
</template> </template>
<style lang='scss' scoped> <style lang='scss' scoped>
.infoBox{ .box-title{
position: relative; display: flex;
} justify-content: space-between;
align-items: center;
.remove-vol { h3 {
position: absolute; margin-bottom: 0;
top: 10px; }
right: 16px;
padding:0px;
max-height: 28px;
min-height: 28px;
} }
</style> </style>

View File

@ -303,55 +303,57 @@ export default {
v-for="(volume, i) in rows" v-for="(volume, i) in rows"
:key="volume.id" :key="volume.id"
> >
<InfoBox class="box"> <InfoBox>
<button <div class="box-title mb-10">
v-if="!isView" <h3>
type="button" <span
class="role-link btn btn-sm remove" v-if="volume.to && isVirtualType"
@click="removeVolume(volume)" class="title"
>
<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"
> >
<i class="icon icon-external-link" /> <router-link :to="volume.to">
</a> {{ t('harvester.virtualMachine.volume.edit') }} {{ headerFor(volume.source) }}
</span> </router-link>
<span v-else> <BadgeStateFormatter
{{ headerFor(volume.source, !!volume?.volumeBackups) }} v-if="volume.pvc"
</span> class="ml-10 state"
</h3> :arbitrary="true"
:row="volume.pvc"
:value="volume.pvc.state"
/>
<a
v-if="dev && !!volume.pvc && !!volume.pvc.resourceExternalLink"
v-clean-tooltip="t(volume.pvc.resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
class="ml-5 resource-external"
rel="nofollow noopener noreferrer"
target="_blank"
:href="volume.pvc.resourceExternalLink.url"
>
<i class="icon icon-external-link" />
</a>
</span>
<span v-else>
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
</span>
</h3>
<button
v-if="!isView"
type="button"
class="role-link btn btn-sm remove"
@click="removeVolume(volume)"
>
<i class="icon icon-x" />
</button>
<button
v-if="volume.hotpluggable && isView"
type="button"
class="role-link btn btn-sm remove"
@click="unplugVolume(volume)"
>
{{ t('harvester.virtualMachine.hotUnplug.detachVolume.actionLabel') }}
</button>
</div>
<div> <div>
<component <component
:is="componentFor(volume.source)" :is="componentFor(volume.source)"
@ -495,25 +497,24 @@ export default {
</template> </template>
<style lang='scss' scoped> <style lang='scss' scoped>
.box { .box-title {
position: relative; display: flex;
justify-content: space-between;
align-items: center;
h3 {
margin-bottom: 0;
}
} }
.title { .title {
display: flex; display: flex;
align-items: center;
.state { .state {
font-size: 16px; font-size: 16px;
} }
} }
.remove {
position: absolute;
top: 10px;
right: 10px;
padding: 0px;
}
.bootOrder { .bootOrder {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -99,7 +99,6 @@ export default {
this.allPVCs this.allPVCs
.filter( (pvc) => { .filter( (pvc) => {
let isAvailable = true; let isAvailable = true;
let isBeingUsed = false;
this.rows.forEach( (O) => { this.rows.forEach( (O) => {
if (O.volumeName === pvc.metadata.name) { if (O.volumeName === pvc.metadata.name) {
@ -111,17 +110,16 @@ export default {
return false; return false;
} }
// already used as image volume
if (this.idx > 0 && pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
return false;
}
if (pvc.isGoldenImageVolume) { if (pvc.isGoldenImageVolume) {
return false; return false;
} }
if (pvc.attachVM && isAvailable && pvc.attachVM?.id === this.vm?.id && this.isEdit) { return isAvailable && pvc.isAvailable;
isBeingUsed = false;
} else if (pvc.attachVM) {
isBeingUsed = true;
}
return isAvailable && !isBeingUsed && pvc.isAvailable;
}) })
.map((pvc) => { .map((pvc) => {
return { return {

View File

@ -96,7 +96,6 @@ export default {
templateVersionId: '', templateVersionId: '',
namePrefix: '', namePrefix: '',
isSingle: true, isSingle: true,
useTemplate: false,
isOpen: false, isOpen: false,
hostname, hostname,
isRestartImmediately, isRestartImmediately,
@ -255,6 +254,7 @@ export default {
return volume; return volume;
}); });
delete cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.MAC_ADDRESS];
cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource); cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
// Update instance labels, labels and annotations // Update instance labels, labels and annotations
@ -490,6 +490,7 @@ export default {
if (this.isSingle) { if (this.isSingle) {
if (!this.value.spec.template.spec.hostname) { if (!this.value.spec.template.spec.hostname) {
this.value.spec.template.spec['hostname'] = this.value.metadata.name; this.value.spec.template.spec['hostname'] = this.value.metadata.name;
this.spec.template.spec['hostname'] = this.value.metadata.name;
} }
} }

View File

@ -154,15 +154,19 @@ harvester:
nodeTimeout: nodeTimeout:
label: Node Collection Timeout label: Node Collection Timeout
tooltip: Minutes allowed for collecting logs/configurations on nodes.<br/>See docs support-bundle-node-collection-timeout for detail. 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 }.' success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
title: Add Volume title: Add Volume
hotplugNic:
success: 'The settings have been saved, but the network interface {interfaceName} will be attached only after the virtual machine is migrated.'
title: Add Network Interface
vmNetwork: Virtual Machine Network
macAddress: MAC Address
macAddressTooltip: If left blank, the MAC address will be automatically generated.
cpuMemoryHotplug: cpuMemoryHotplug:
success: 'CPU and Memory are updated to the virtual machine { vm }.' success: 'CPU and Memory are updated to the virtual machine { vm }.'
title: Edit CPU and Memory title: Edit CPU and Memory
maxResourcesMessage: 'You can increase the CPU to maximum { maxCpu }C and memory to maximum { maxMemory }.' maxResourcesMessage: 'You can increase the CPU to maximum { maxCpu }C and memory to maximum { maxMemory }.'
hotunplug:
success: 'Volume { name } is detached successfully.'
snapshot: snapshot:
title: Take Snapshot title: Take Snapshot
name: Name name: Name
@ -226,7 +230,8 @@ harvester:
disableCPUManager: Disable CPU Manager disableCPUManager: Disable CPU Manager
cordon: Cordon cordon: Cordon
uncordon: Uncordon uncordon: Uncordon
addHotplug: Add Volume addHotplugVolume: Add Volume
addHotplugNic: Hotplug Network Interface
exportImage: Export Image exportImage: Export Image
viewlogs: View Logs viewlogs: View Logs
cancelExpand: Cancel Expand cancelExpand: Cancel Expand
@ -442,6 +447,7 @@ harvester:
storage: Storage storage: Storage
labels: Labels labels: Labels
ksmtuned: Ksmtuned ksmtuned: Ksmtuned
hugepages: Hugepages
seeder: Out-of-band Access seeder: Out-of-band Access
detail: detail:
kvm: kvm:
@ -514,6 +520,20 @@ harvester:
fullScans: Full Scans fullScans: Full Scans
stableNodeChains: Stable Node Chains stableNodeChains: Stable Node Chains
stableNodeDups: Stable Node Dups 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: disk:
add: Add Disk add: Add Disk
path: path:
@ -582,6 +602,16 @@ harvester:
title: Enable CPU and memory hotplug title: Enable CPU and memory hotplug
tooltip: The default maximum CPU and maximum memory are {hotPlugTimes} times based on CPU and memory. 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 restartVMMessage: Restart action is required for the virtual machine configuration change to take effect
hotUnplug:
actionLabel: Detach
detachVolume:
title: 'Are you sure that you want to detach volume {name}?'
actionLabel: Detach Volume
success: 'Volume { name } is detached successfully.'
detachNIC:
title: 'Are you sure that you want to detach network interface {name}?'
actionLabel: Detach Network Interface
success: 'The settings have been saved, but the network interface {name} will be detached only after the virtual machine is migrated.'
instance: instance:
singleInstance: singleInstance:
multipleInstance: multipleInstance:
@ -613,11 +643,6 @@ harvester:
title: 'Select the volume you want to delete:' title: 'Select the volume you want to delete:'
deleteAll: Delete All 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." 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: |- restartTip: |-
{restart, select, {restart, select,
true {Restart} true {Restart}
@ -897,6 +922,8 @@ harvester:
checksumTip: Validate the image using the SHA512 checksum, if specified. checksumTip: Validate the image using the SHA512 checksum, if specified.
tooltip: tooltip:
imported: Created automatically by the vm-import-controller imported: Created automatically by the vm-import-controller
errors:
unsupportedBackend: 'Unsupported backend type: {backend}'
vmTemplate: vmTemplate:
label: Templates label: Templates
@ -1064,6 +1091,8 @@ harvester:
placeholder: e.g. 172.16.0.0/16 placeholder: e.g. 172.16.0.0/16
excludeIPs: 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. 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: acl:
label: Access Control List label: Access Control List
tooltip: The ACL to apply to this Subnet. Must be one of the ACLs in the same namespace. tooltip: The ACL to apply to this Subnet. Must be one of the ACLs in the same namespace.
@ -1179,7 +1208,7 @@ harvester:
vlan: VLAN ID vlan: VLAN ID
exclude: exclude:
label: Exclude IPs 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.' invalid: '"Exclude list" is invalid.'
addIp: Add Exclude IP 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.' 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.'
@ -1265,6 +1294,12 @@ harvester:
retention: How long to retain metrics retention: How long to retain metrics
retentionSize: Maximum size of metrics retentionSize: Maximum size of metrics
clusterRegistrationUrl: 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. 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: ntpServers:
isNotIPV4: The address you entered is not IPv4 or host. Please enter a valid IPv4 address or a host address. isNotIPV4: The address you entered is not IPv4 or host. Please enter a valid IPv4 address or a host address.
@ -1534,6 +1569,7 @@ 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-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-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. '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: vmImport:
titles: titles:
basic: Basic basic: Basic
@ -1695,7 +1731,7 @@ harvester:
available: Available available: Available
total: Total total: Total
vGPUID: vGPU ID vGPUID: vGPU ID
goSriovGPU: goSriovGPU:
prefix: Please enable the supported GPU devices in prefix: Please enable the supported GPU devices in
middle: SR-IOV GPU Devices middle: SR-IOV GPU Devices
suffix: page to manage the vGPU MIG configurations. suffix: page to manage the vGPU MIG configurations.

View File

@ -142,6 +142,7 @@ export default {
showYaml: false, showYaml: false,
spec: null, spec: null,
osType: 'linux', osType: 'linux',
useTemplate: false,
sshKey: [], sshKey: [],
maintenanceStrategies, maintenanceStrategies,
maintenanceStrategy: 'Migrate', maintenanceStrategy: 'Migrate',
@ -273,7 +274,7 @@ export default {
needNewSecret() { needNewSecret() {
// When creating a template it is always necessary to create a new secret. // When creating a template it is always necessary to create a new secret.
return this.isCreate ? true : this.showYaml ? false : this.resourceType === HCI.VM_VERSION; return this.isCreate || this.showYaml ? false : this.resourceType === HCI.VM_VERSION;
}, },
defaultTerminationSetting() { defaultTerminationSetting() {
@ -689,7 +690,6 @@ export default {
set(this.spec.template.spec, 'domain.memory.maxGuest', this.maxMemory); set(this.spec.template.spec, 'domain.memory.maxGuest', this.maxMemory);
set(this.spec.template.spec, 'domain.resources.limits.memory', this.maxMemory); set(this.spec.template.spec, 'domain.resources.limits.memory', this.maxMemory);
} else { } else {
this.spec.template.spec.domain.cpu.maxSockets = 1;
this.spec.template.spec.domain.cpu.sockets = 1; this.spec.template.spec.domain.cpu.sockets = 1;
this.spec.template.spec.domain.cpu.cores = this.cpu; this.spec.template.spec.domain.cpu.cores = this.cpu;
this.spec.template.spec.domain.resources.limits.cpu = this.cpu?.toString(); this.spec.template.spec.domain.resources.limits.cpu = this.cpu?.toString();
@ -709,16 +709,7 @@ export default {
disk.forEach( (R, index) => { disk.forEach( (R, index) => {
const prefixName = this.value.metadata?.name || ''; const prefixName = this.value.metadata?.name || '';
const dataVolumeName = this.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;
}
const _disk = this.parseDisk(R, index); const _disk = this.parseDisk(R, index);
const _volume = this.parseVolume(R, dataVolumeName); const _volume = this.parseVolume(R, dataVolumeName);
@ -1013,6 +1004,20 @@ export default {
this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled; 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) { parseDisk(R, index) {
const out = { name: R.name }; const out = { name: R.name };
@ -1638,7 +1643,8 @@ export default {
secretRef: { secretRef: {
handler(secret) { 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; this.secretName = secret?.metadata.name;
} }
}, },

View File

@ -1,12 +1,12 @@
import SteveModel from '@shell/plugins/steve/steve-class';
import { escapeHtml } from '@shell/utils/string'; import { escapeHtml } from '@shell/utils/string';
import { colorForState } from '@shell/plugins/dashboard-store/resource-class'; import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
import HarvesterResource from './harvester';
/** /**
* Class representing vGPU MIGConfiguration resource. * Class representing vGPU MIGConfiguration resource.
* @extends SteveModal * @extends HarvesterResource
*/ */
export default class MIGCONFIGURATION extends SteveModel { export default class MIGCONFIGURATION extends HarvesterResource {
get _availableActions() { get _availableActions() {
let out = super._availableActions; let out = super._availableActions;

View File

@ -354,7 +354,7 @@ export default class HciNode extends HarvesterResource {
} }
get isCordoned() { get isCordoned() {
return (this.isUnSchedulable && !this.isEtcd) || this.hasAction('uncordon'); return this.hasAction('uncordon');
} }
get isEtcd() { get isEtcd() {

View File

@ -35,15 +35,10 @@ export default class HciPv extends HarvesterResource {
get availableActions() { get availableActions() {
let out = super._availableActions; let out = super._availableActions;
// Longhorn V2 provisioner do not support volume clone feature yet const clone = out.find((action) => action.action === 'goToClone');
if (this.isLonghornV2) {
out = out.filter((action) => action.action !== 'goToClone');
} else {
const clone = out.find((action) => action.action === 'goToClone');
if (clone) { if (clone) {
clone.action = 'goToCloneVolume'; clone.action = 'goToCloneVolume';
}
} }
const exportImageAction = { const exportImageAction = {
@ -65,10 +60,6 @@ export default class HciPv extends HarvesterResource {
takeSnapshotAction, takeSnapshotAction,
...out ...out
]; ];
// TODO: remove this block if Longhorn V2 engine supports restore volume snapshot
if (this.isLonghornV2) {
out = out.filter((action) => action.action !== takeSnapshotAction.action);
}
} else { // v1.4 / v1.3 } else { // v1.4 / v1.3
if (!this.isLonghorn || !this.isLonghornV2) { if (!this.isLonghorn || !this.isLonghornV2) {
out = [ out = [

View File

@ -113,8 +113,9 @@ export default class HciAddonConfig extends HarvesterResource {
get displayName() { get displayName() {
const isExperimental = this.metadata?.labels?.[HCI_ANNOTATIONS.ADDON_EXPERIMENTAL] === 'true'; const isExperimental = this.metadata?.labels?.[HCI_ANNOTATIONS.ADDON_EXPERIMENTAL] === 'true';
const name = this.metadata?.labels?.[HCI_ANNOTATIONS.ADDON_DISPLAYNAME] || this.metadata.name;
return isExperimental ? `${ this.metadata.name } (${ this.t('generic.experimental') })` : this.metadata.name; return isExperimental ? `${ name } (${ this.t('generic.experimental') })` : name;
} }
get customValidationRules() { get customValidationRules() {

View File

@ -52,11 +52,19 @@ export default class HciSetting extends HarvesterResource {
}); });
} }
get clusterRegistrationTLSVerifyFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('clusterRegistrationTLSVerify');
}
get customValue() { get customValue() {
if (this.metadata.name === HCI_SETTING.STORAGE_NETWORK) { if (this.metadata.name === HCI_SETTING.STORAGE_NETWORK) {
try { try {
return JSON.stringify(JSON.parse(this.value), null, 2); return JSON.stringify(JSON.parse(this.value), null, 2);
} catch (e) {} } catch (e) {}
} else if (this.metadata.name === HCI_SETTING.CLUSTER_REGISTRATION_URL) {
try {
return this.clusterRegistrationTLSVerifyFeatureEnabled ? JSON.stringify(JSON.parse(this.value), null, 2) : this.value;
} catch (e) {}
} }
return false; return false;

View File

@ -318,8 +318,21 @@ export default class HciVmImage extends HarvesterResource {
get uploadImage() { get uploadImage() {
return async(file, opt = {}) => { return async(file, opt = {}) => {
const formData = new FormData(); const formData = new FormData();
const backend = this.spec?.backend || 'backingimage';
const backendFieldMap = {
cdi: 'file',
backingimage: 'chunk'
};
const fieldName = backendFieldMap[backend];
formData.append('chunk', file); if (!fieldName) {
const error = this.t('harvester.image.errors.unsupportedBackend', { backend });
this.$ctx.commit('harvester-common/uploadError', { name: this.name, message: error }, { root: true });
throw new Error(error);
}
formData.append(fieldName, file);
try { try {
this.$ctx.commit('harvester-common/uploadStart', this.metadata.name, { root: true }); this.$ctx.commit('harvester-common/uploadStart', this.metadata.name, { root: true });

View File

@ -29,6 +29,16 @@ export default class NetworkAttachmentDef extends SteveModel {
} }
} }
get isSystem() {
const systemNamespaces = this.$rootGetters['systemNamespaces'];
if (systemNamespaces.includes(this.metadata?.namespace)) {
return true;
}
return false;
}
get isIpamStatic() { get isIpamStatic() {
return this.parseConfig.ipam?.type === 'static'; return this.parseConfig.ipam?.type === 'static';
} }

View File

@ -87,17 +87,11 @@ const IgnoreMessages = ['pod has unbound immediate PersistentVolumeClaims'];
export default class VirtVm extends HarvesterResource { export default class VirtVm extends HarvesterResource {
get availableActions() { get availableActions() {
let out = super._availableActions; const out = super._availableActions;
const clone = out.find((action) => action.action === 'goToClone');
// VM attached with Longhorn V2 volume doesn't support clone feature if (clone) {
if (this.longhornV2Volumes.length > 0) { clone.action = 'goToCloneVM';
out = out.filter((action) => action.action !== 'goToClone');
} else {
const clone = out.find((action) => action.action === 'goToClone');
if (clone) {
clone.action = 'goToCloneVM';
}
} }
return [ return [
@ -159,7 +153,7 @@ export default class VirtVm extends HarvesterResource {
}, },
{ {
action: 'takeVMSnapshot', action: 'takeVMSnapshot',
enabled: (!!this.actions?.snapshot || !!this.action?.backup) && !this.longhornV2Volumes.length, enabled: (!!this.actions?.snapshot || !!this.action?.backup),
icon: 'icon icon-snapshot', icon: 'icon icon-snapshot',
label: this.t('harvester.action.vmSnapshot') label: this.t('harvester.action.vmSnapshot')
}, },
@ -206,10 +200,16 @@ export default class VirtVm extends HarvesterResource {
label: this.t('harvester.action.abortMigration') label: this.t('harvester.action.abortMigration')
}, },
{ {
action: 'addHotplug', action: 'addHotplugVolume',
enabled: !!this.actions?.addVolume, enabled: !!this.actions?.addVolume,
icon: 'icon icon-plus', icon: 'icon icon-plus',
label: this.t('harvester.action.addHotplug') label: this.t('harvester.action.addHotplugVolume')
},
{
action: 'addHotplugNic',
enabled: this.hotplugNicFeatureEnabled && !!this.actions?.addNic,
icon: 'icon icon-plus',
label: this.t('harvester.action.addHotplugNic')
}, },
{ {
action: 'createTemplate', action: 'createTemplate',
@ -395,8 +395,20 @@ export default class VirtVm extends HarvesterResource {
this.$dispatch('promptModal', { this.$dispatch('promptModal', {
resources, resources,
diskName, name: diskName,
component: 'HarvesterUnplugVolume' type: 'volume',
component: 'HarvesterHotUnplug',
});
}
unplugNIC(networkName) {
const resources = this;
this.$dispatch('promptModal', {
resources,
name: networkName,
type: 'network',
component: 'HarvesterHotUnplug',
}); });
} }
@ -504,10 +516,17 @@ export default class VirtVm extends HarvesterResource {
}); });
} }
addHotplug(resources = this) { addHotplugVolume(resources = this) {
this.$dispatch('promptModal', { this.$dispatch('promptModal', {
resources, resources,
component: 'HarvesterAddHotplugModal' component: 'HarvesterAddHotplugVolumeModal'
});
}
addHotplugNic(resources = this) {
this.$dispatch('promptModal', {
resources,
component: 'HarvesterAddHotplugNic'
}); });
} }
@ -1240,6 +1259,10 @@ export default class VirtVm extends HarvesterResource {
return this.$rootGetters['harvester-common/getFeatureEnabled']('vmMachineTypeAuto'); return this.$rootGetters['harvester-common/getFeatureEnabled']('vmMachineTypeAuto');
} }
get hotplugNicFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('hotplugNic');
}
get isBackupTargetUnavailable() { get isBackupTargetUnavailable() {
const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || []; const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || [];
const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target'); const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target');

View File

@ -1,7 +1,7 @@
{ {
"name": "harvester", "name": "harvester",
"description": "Rancher UI Extension for Harvester", "description": "Rancher UI Extension for Harvester",
"version": "1.7.0-dev", "version": "1.7.2-rc1",
"private": false, "private": false,
"rancher": { "rancher": {
"annotations": { "annotations": {
@ -29,4 +29,4 @@
"last 2 versions", "last 2 versions",
"not dead" "not dead"
] ]
} }

View File

@ -79,7 +79,6 @@ export default {
skipSingleReplicaDetachedVolFeatureEnabled() { skipSingleReplicaDetachedVolFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol'); return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
}, },
allOSImages() { allOSImages() {
return this.$store.getters['harvester/all'](HCI.IMAGE).filter((I) => I.isOSImage) || []; return this.$store.getters['harvester/all'](HCI.IMAGE).filter((I) => I.isOSImage) || [];
}, },
@ -116,7 +115,7 @@ export default {
}, },
fileName() { fileName() {
return this.file?.name || ''; return this.preprocessImageName(this.file?.name || '');
}, },
canEnableLogging() { canEnableLogging() {
@ -191,6 +190,7 @@ export default {
annotations: {} annotations: {}
}, },
spec: { spec: {
backend: 'cdi',
sourceType: UPLOAD, sourceType: UPLOAD,
displayName: '', displayName: '',
checksum: this.imageValue?.spec?.checksum || '', checksum: this.imageValue?.spec?.checksum || '',
@ -203,8 +203,9 @@ export default {
this.file = {}; this.file = {};
this.errors = []; this.errors = [];
const imageDisplayName = this.imageValue?.spec?.displayName || '';
if (!this.imageValue.spec.displayName && this.createNewImage) { if (!imageDisplayName && this.createNewImage) {
this.errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('generic.name') })); this.errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('generic.name') }));
buttonCb(false); buttonCb(false);
@ -212,24 +213,29 @@ export default {
} }
try { try {
// Save the image first if creating a new one
if (this.imageSource === IMAGE_METHOD.NEW) { if (this.imageSource === IMAGE_METHOD.NEW) {
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True'; this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
if (this.sourceType === UPLOAD && this.uploadImageId !== '') { // upload new image if (this.sourceType === UPLOAD && this.uploadImageId !== '') { // upload new image
this.value.spec.image = this.uploadImageId; this.value.spec.image = this.uploadImageId;
} else if (this.sourceType === DOWNLOAD) { // give URL to download new image } else if (this.sourceType === DOWNLOAD) { // give URL to download new image
this.imageValue.spec.sourceType = DOWNLOAD; // check if URL is provided
if (!this.imageValue.spec.url) { if (!this.imageValue.spec.url) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.imageUrl')); this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.imageUrl'));
buttonCb(false); buttonCb(false);
return; return;
} }
this.imageValue.spec.sourceType = DOWNLOAD;
this.imageValue.spec.targetStorageClassName = 'longhorn-static';
res = await this.imageValue.save(); res = await this.imageValue.save();
this.value.spec.image = res.id; this.value.spec.image = res.id;
} }
} else if (this.imageSource === IMAGE_METHOD.EXIST) { } else if (this.imageSource === IMAGE_METHOD.EXIST) { // select existing image
if (!this.imageId) { if (!this.imageId) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.chooseFile')); this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.chooseFile'));
buttonCb(false); buttonCb(false);
@ -239,7 +245,7 @@ export default {
this.value.spec.image = this.imageId; this.value.spec.image = this.imageId;
} }
// enable logging or skip single replica detection if checked
if (this.canEnableLogging) { if (this.canEnableLogging) {
this.value.spec.logEnabled = this.enableLogging; this.value.spec.logEnabled = this.enableLogging;
} }
@ -256,7 +262,7 @@ export default {
}, },
async uploadFile(file) { async uploadFile(file) {
const fileName = file.name; const fileName = this.preprocessImageName(file.name);
if (!fileName) { if (!fileName) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.unknownImageName')); this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.unknownImageName'));
@ -280,6 +286,8 @@ export default {
this.imageValue.spec.url = ''; this.imageValue.spec.url = '';
try { try {
this.imageValue.spec.targetStorageClassName = 'longhorn-static';
const res = await this.imageValue.save(); const res = await this.imageValue.save();
this.uploadImageId = res.id; this.uploadImageId = res.id;
@ -301,15 +309,25 @@ export default {
} }
}, },
// replace _ to - to meet storage class name requirement
preprocessImageName(name) {
if (!name) {
return '';
}
return name.toLowerCase().replace(/[_]/g, '-');
},
handleImageDelete(imageId) { handleImageDelete(imageId) {
const image = this.allOSImages.find((I) => I.id === imageId); const image = this.allOSImages.find((I) => I.id === imageId);
const imageDisplayName = image?.spec?.displayName || '';
if (image) { if (image && imageDisplayName) {
this.$store.dispatch('harvester/promptModal', { this.$store.dispatch('harvester/promptModal', {
resources: [image], resources: [image],
component: 'ConfirmRelatedToRemoveDialog', component: 'ConfirmRelatedToRemoveDialog',
needConfirmation: false, needConfirmation: false,
warningMessage: this.$store.getters['i18n/t']('harvester.modal.osImage.message', { name: image.displayName }) warningMessage: this.$store.getters['i18n/t']('harvester.modal.osImage.message', { name: imageDisplayName }),
}); });
this.deleteImageId = ''; this.deleteImageId = '';
} }
@ -419,13 +437,13 @@ export default {
v-if="showUploadSuccessBanner" v-if="showUploadSuccessBanner"
color="success" color="success"
class="mt-0 mb-30" class="mt-0 mb-30"
:label="t('harvester.setting.upgrade.uploadSuccess', { name: file.name })" :label="t('harvester.setting.upgrade.uploadSuccess', { name: fileName })"
/> />
<Banner <Banner
v-if="showUploadingWarningBanner" v-if="showUploadingWarningBanner"
color="warning" color="warning"
class="mt-0 mb-30" class="mt-0 mb-30"
:label="t('harvester.image.warning.osUpgrade.uploading', { name: file.name })" :label="t('harvester.image.warning.osUpgrade.uploading', { name: fileName })"
/> />
<div <div

View File

@ -98,6 +98,11 @@ export default {
if (getters['schemaFor'](HCI.UPGRADE)) { if (getters['schemaFor'](HCI.UPGRADE)) {
hash.upgrades = dispatch('findAll', { type: HCI.UPGRADE }); hash.upgrades = dispatch('findAll', { type: HCI.UPGRADE });
} }
// Pre-fetch all HCI.UPGRADE_LOG data within loadCluster to ensure HarvesterUpgradeHeader has the necessary data. This is required because the header is dynamically loaded before the user enters the cluster in Rancher integration mode.
// See more details in https://github.com/harvester/harvester-ui-extension/pull/715
if (getters['schemaFor'](HCI.UPGRADE_LOG)) {
hash.upgradeLogs = dispatch('findAll', { type: HCI.UPGRADE_LOG });
}
const res: any = await allHash(hash); const res: any = await allHash(hash);

View File

@ -37,6 +37,7 @@ export const HCI = {
STORAGE: 'harvesterhci.io.storage', STORAGE: 'harvesterhci.io.storage',
RESOURCE_QUOTA: 'harvesterhci.io.resourcequota', RESOURCE_QUOTA: 'harvesterhci.io.resourcequota',
KSTUNED: 'node.harvesterhci.io.ksmtuned', KSTUNED: 'node.harvesterhci.io.ksmtuned',
HUGEPAGES: 'node.harvesterhci.io.hugepage',
PCI_DEVICE: 'devices.harvesterhci.io.pcidevice', PCI_DEVICE: 'devices.harvesterhci.io.pcidevice',
PCI_CLAIM: 'devices.harvesterhci.io.pcideviceclaim', PCI_CLAIM: 'devices.harvesterhci.io.pcideviceclaim',
SR_IOV: 'devices.harvesterhci.io.sriovnetworkdevice', SR_IOV: 'devices.harvesterhci.io.sriovnetworkdevice',

1592
yarn.lock

File diff suppressed because it is too large Load Diff