Compare commits

..
39 Commits
Author SHA1 Message Date
mergify[bot]andAndy Lee c562dd73ba chore: bump to 1.6.0-rc5 (#450) (#451)
(cherry picked from commit 57dfddd593)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-04 21:34:33 +08:00
mergify[bot]andAndy Lee 1cd5e5ca7d feat: add restore failed message in VM state description (#448) (#449)
(cherry picked from commit 66eac96575)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-04 14:11:58 +08:00
mergify[bot]andAndy Lee 1aaa8f71c4 fix: featureFlags is undefined when installing lower version of ui-ext (#443) (#447)
* fix: featureFlags is undefined if install lower version of ui-ext

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

(cherry picked from commit 182d92d80b)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-08-01 15:41:10 +08:00
mergify[bot]andYiya Chen 003a09d0a9 fix: update cdiSettings (#444) (#445)
(cherry picked from commit 1b362f570b)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-08-01 12:15:10 +08:00
mergify[bot]andAndy Lee f08e632aad chore: bump version to v1.6.0-rc4 (#439) (#442)
* chore: bump version to v1.6.0-rc4



* fix: missing vm network migration setting



---------


(cherry picked from commit cf798048f8)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-30 16:07:47 +08:00
mergify[bot]andAndy Lee df1d7001bc fix: cpu/ memory columns in host detail VM tab (#436) (#437)
(cherry picked from commit 93c8399935)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-30 11:57:52 +08:00
mergify[bot]andAndy Lee 41f78c83a7 fix: isStopping state condition (#431) (#435)
* fix: isStopping state condition



* refactor: remove unused variable



---------


(cherry picked from commit b72f523ddf)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-29 13:02:30 +08:00
mergify[bot]andAndy Lee f1a53311ce chore: upgrade rancher/shell to 3.0.5-rc.7 (#429) (#432)
(cherry picked from commit e5ffb08df3)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-29 10:00:10 +08:00
mergify[bot]andAndy Lee e0c6e905a9 refactor: filter anchor link in setting description when searching (#428) (#430)
(cherry picked from commit 98a6322c11)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-28 18:26:19 +08:00
mergify[bot]andAndy Lee 1852e434f1 feat: add runStragety field when cloning a VM (#424) (#427)
* feat: add runStragety field when cloning a VM



* refactor: remove unneeded change



---------


(cherry picked from commit be7e4bd80b)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-28 14:31:18 +08:00
mergify[bot] 77c085d3a7 feat: expose CDI settings (#418) (#425)
* feat: add cdi settings tab



* feat: basic layout



* feat: add fields mutation



* refactor: rename keys



* refactor: add edit mode



* refactor: remove isCreate



* feat: filter volume mode options



---------

(cherry picked from commit ed2bc3100b)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-07-25 13:59:50 +08:00
mergify[bot]andYiya Chen 9dbe7d9015 fix: exception error (#422) (#423)
(cherry picked from commit b6ffb3e9f1)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-24 13:00:44 +08:00
mergify[bot]andAndy Lee 3f19494d98 chore: bump version to 1.6.0-rc3 (#420) (#421)
(cherry picked from commit 3d8a6bba7e)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-23 15:11:41 +08:00
mergify[bot]andAndy Lee 296601d38c feat: CPU / Memory hotplug support (#413) (#419)
* feat: add maxCPU and maxMemory



* feat: add hotplug dialog



* feat: add restart message



* feat: let VM template support cpuMemoryHotplug



* feat: add feature flag



* feat: add max-hotplug-ratio setting



---------


(cherry picked from commit be9311dc0c)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-23 14:11:08 +08:00
mergify[bot]andAndy Lee 5d9ba584b8 refactor: remove unneeded translation keys (#416) (#417)
(cherry picked from commit e294f4c00f)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-21 17:47:32 +08:00
eab4ba3d0f feat: Remove guest cluster in Rancher (#391) (#415)
* feat: Remove guest cluster in Rancher



* feat: add feature flag 1.6.0



* feat: update for review



* chore: fix for review



* chore: fix for review



* refactor: reduce redundant code



* chore: change text area to yaml editor



* refactor: change radio and yaml editor position



---------




(cherry picked from commit a9fa928912)

Signed-off-by: Nick Chung <nick.chung@suse.com>
Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Nick Chung <Nick.Chung@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-21 17:28:34 +08:00
mergify[bot]andYiya Chen 4e5320dc72 feat: disable vm volume resize (#409) (#412)
(cherry picked from commit ef2b4d1589)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-17 18:33:15 +08:00
mergify[bot]andAndy Lee ba7092f400 ci: disable subject-max-length commitlint rule (#408) (#411)
(cherry picked from commit a73e9f0ac1)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-17 13:54:15 +08:00
mergify[bot]andNick Chung eec7c3573d chore: disable vmstate-persistence and longhorn-static StorageClasses (backport #377) (#405)
* chore: disable vmstate-persistence and longhorn-static StorageClasses (#377)

* chore: disable vmstate-persistence and longhorn-static StorageClasses

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

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

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

* chore: remove deletion tooltips in image and volume pages

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

* chore: rollback style changes of image and volume lists

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

---------

Signed-off-by: Nick Chung <nick.chung@suse.com>
(cherry picked from commit b775ce5f50)

* chore: fix missing semicolon in types.js

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

---------

Signed-off-by: Nick Chung <nick.chung@suse.com>
Signed-off-by: Nick Chung <Nick.Chung@suse.com>
Co-authored-by: Nick Chung <Nick.Chung@suse.com>
2025-07-16 18:32:13 +08:00
mergify[bot]andAndy Lee f0014cc627 chore: bump version to v1.6.0-rc2 (#403) (#404)
(cherry picked from commit 193d4536e3)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-16 18:13:20 +08:00
mergify[bot]andAndy Lee 9aa311a7e3 feat: support for subnets and VPCs from UI (#374) (#402)
(cherry picked from commit 15950eb209165a980ec53d22fe2898b94d6b1c8b)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-16 17:48:48 +08:00
mergify[bot]andYiya Chen 013d9f1d7e fix: add default value to prevent exception (#397) (#401)
(cherry picked from commit bcabefe9f3)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-16 15:19:21 +08:00
mergify[bot]andAndy Lee 8c044ba44c ci: add package version check (#396) (#400)
* ci: add package version check



* ci: extract check version in a script



* ci: update env variable



---------


(cherry picked from commit c541f81dc3)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-16 13:21:09 +08:00
mergify[bot]andYiya Chen 7b8592e07d feat: add vm-migration-network setting (#395) (#398)
(cherry picked from commit 4486f71c8f)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-16 10:22:31 +08:00
mergify[bot]andYiya Chen 53d2e67b4f chore: upgrade rancher/shell to 3.0.5-rc6 (#393) (#394)
* chore: bump shell rc6
* chore: add rancher restriction

---------


(cherry picked from commit 060105ead3)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-14 10:01:12 +08:00
mergify[bot]andAndy Lee 1d146a40bd fix: find the correct vmim based on creationTimeStamp (#389) (#390)
(cherry picked from commit 3ea73978ee)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-10 11:22:48 +08:00
mergify[bot]andYiya Chen 11d6943596 feat: add csi-online-expand-validation setting (#378) (#388)
* feat: add csi-online-expand-validation setting
* feat: invalid json error message
* feat: handle API errors
* refactor: remove inStore()

---------


(cherry picked from commit f4e363396d)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-09 15:31:17 +08:00
mergify[bot]andNick Chung 7b754457ec chore: remove run-lint from build-standalone-on-release (#355) (#387)
(cherry picked from commit 7386a2deb6)

Signed-off-by: Nick Chung <nick.chung@suse.com>
Co-authored-by: Nick Chung <Nick.Chung@suse.com>
2025-07-09 14:51:47 +08:00
mergify[bot]andAndy Lee 2cb838eded chore: bump version to v1.6.0-rc1 (#385) (#386)
(cherry picked from commit fa16e24983)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-09 14:36:39 +08:00
mergify[bot]andYiya Chen bdcb34ab14 ci: ignore draft PR add add backport labels (#382) (#384)
* ci: skip draft PR
* ci: fix auto assign
* ci: add backport label
* fix: remove token

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
(cherry picked from commit ee28697161)

# Conflicts:
#	.github/workflows/backport-label.yaml

Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-09 13:28:54 +08:00
mergify[bot]andAndy Lee 8daf44b3f6 fix: read volume accessMode even is RWO (#381) (#383)
(cherry picked from commit ce63ee6a19)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-09 12:07:41 +08:00
mergify[bot]andYiya Chen 8044e74102 feat: disable vm images (#376) (#380)
(cherry picked from commit 85a06feb91)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-08 17:10:14 +08:00
mergify[bot]andNick Chung 264d65cc6d fix: can't migrate network config in Rancher integration (#371) (#373)
(cherry picked from commit 0ef4ff65cc)

Signed-off-by: Nick Chung <nick.chung@suse.com>
Co-authored-by: Nick Chung <Nick.Chung@suse.com>
2025-07-03 16:59:13 +08:00
mergify[bot]andAndy Lee 342a82c8e0 fix: add custom promptRemove for backup and snasphot deletion (#367) (#372)
(cherry picked from commit f12717a8f4)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-03 14:06:40 +08:00
mergify[bot]andNick Chung e7cd5cc4b0 feat: [ENHANCEMENT] Add setting upgrade-config to UI(#8369) (#347) (#363)
(cherry picked from commit dc683a50a4)

Signed-off-by: Nick Chung <nick.chung@suse.com>
Co-authored-by: Nick Chung <Nick.Chung@suse.com>
2025-07-01 16:24:50 +08:00
mergify[bot]andAndy Lee cf405be71e fix: cloned secret should keep originl type (#354) (#362)
(cherry picked from commit 1d89aafeab)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-01 11:47:40 +08:00
mergify[bot]andYiya Chen 911d30d7ba feat: SB Enhancements (NS selection and timeout) (#345) (#360)
* feat: add namespace field
* feat: add optional inputs
* feat: refine code
* feat: add feature flag
* refactor: fix lint error
* feat: filter default namespaces
* refactor: hide tips with feature flag
* refactor: use UnitInput
* feat: load default value from settings
* refactor: fix API url
* refactor: no available namespaces
* chore: update subject-case rule
---------
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

(cherry picked from commit be421054d8)

Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-01 09:53:30 +08:00
mergify[bot]andAndy Lee 1b4bfa0962 ci: replace tag with github.event.release.tag_name (#356) (#359)
(cherry picked from commit fcef0391bb)

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

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