Compare commits

..
51 Commits
Author SHA1 Message Date
mergify[bot]andAndy Lee b8b4ee4c41 replace tag format v1.5.0 to harvester-1.5.0 (#190) (#313)
(cherry picked from commit 374b904191)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-05-26 14:21:47 +08:00
Andy Lee 021d6f673f bump version to 1.0.6
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-26 13:48:57 +08:00
Andy Lee a849671034 bump to v1.0.6-rc4
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-23 11:19:39 +08:00
mergify[bot]andYiya Chen b98f878c5d fix: range condition (#299) (#300)
(cherry picked from commit cbfcf4dbae)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-05-15 16:26:48 +08:00
mergify[bot]andJack Yu 78482285b3 chore: use default imag repo name (#212) (#291)
(cherry picked from commit de4edbbf3b)

Signed-off-by: Jack Yu <jack.yu@suse.com>
Co-authored-by: Jack Yu <jack.yu@suse.com>
2025-05-13 12:10:05 +08:00
mergify[bot]andJack Yu 4656976b91 chore: add building catalog GA (#181) (#290)
* chore: add building catalog GA



* feat: should include prerelease



---------


(cherry picked from commit 1880043a80)

Signed-off-by: Jack Yu <jack.yu@suse.com>
Co-authored-by: Jack Yu <jack.yu@suse.com>
2025-05-13 12:08:06 +08:00
renovate[bot] aeb3588b83 chore(deps): update dependency follow-redirects to v1.15.6 [security] (#287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-13 04:07:25 +00:00
renovate[bot] d14b0cb95c chore(deps): update dependency ejs to v3.1.10 [security] (#286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-13 03:10:36 +00:00
Andy Lee 2f0884d7bd bump to v1.0.6-rc3
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-13 11:07:05 +08:00
mergify[bot]andAndy Lee 037ce6132b VM Access Credentials SSH Keys Not Rendering (backport #279) (#281)
* Remove unneeded condition (#279)

Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 8410f75859)

# Conflicts:
#	pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineSSHKey.vue

* solve conflict

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-05-12 10:56:49 +08:00
Andy Lee 47b54b4f06 bump version to 1.0.6-rc2
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-08 17:35:08 +08:00
Andy Lee 9dd8ae479a fix #cloud-config disappear when add/remove ssh key (#273)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-08 17:32:35 +08:00
Andy Lee 5682bf4c3a Fix create ssh key modal not pop up in rancher integration mode (#269)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-07 15:50:29 +08:00
Andy Lee 960df31667 Bump version to 1.0.6-rc1
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-06 11:57:48 +08:00
Yi-Ya Chen 63451e7d2b ci: rename file
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-06 11:56:30 +08:00
Yi-Ya Chen 55aaa8440d ci: revert change
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-06 11:56:29 +08:00
Yi-Ya Chen da4121da69 ci: rename the workflow 2025-05-06 11:56:29 +08:00
Yi-Ya Chen 30e49a6f2c ci: add ci pipeline
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-05-06 11:56:28 +08:00
Andy Lee 2255bd4f69 Add 1.4.3 feature flag
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-05-06 11:26:53 +08:00
Andy Lee 7e5ee04637 fix template page should be groupable (#262)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-17 15:45:40 +08:00
mergify[bot]andAndy Lee 8863de8874 fix the doneOverride path for harvesterhci.io.virtualmachinetemplate (#211) (#261)
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 22a032e56c)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-04-17 15:40:34 +08:00
mergify[bot]andAndy Lee 73952a6b4e update RadioGroup update value action (#255) (#259)
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 74300f24b3)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-04-17 15:29:29 +08:00
mergify[bot]andAndy Lee 0496fae623 fix percent typo (#258)
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 487f9abc10)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-04-17 15:25:00 +08:00
mergify[bot]andandy.lee d5cd97aafd Fix upgrade cluster model styles (backport #162) (#257)
* fix upgrade model style

Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit ff439e66a9)

* change overallMessage color to red

Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 50901a2eaf)

---------

Co-authored-by: andy.lee <andy.lee@suse.com>
2025-04-17 15:23:32 +08:00
mergify[bot]andYi-Ya Chen 31c6a794be fix: register upgrade component in header (#256)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
(cherry picked from commit b499f62c56)

Co-authored-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-04-17 15:19:08 +08:00
mergify[bot]andandy.lee 9d9383ba90 fix: vGPU number not correct (#254)
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 26afffc2f1)
(cherry picked from commit 40dc31226b)

Co-authored-by: andy.lee <andy.lee@suse.com>
2025-04-17 14:56:05 +08:00
mergify[bot]andandy.lee 1e02dbffe3 fix: isEncrypted breaks all the volume page (#253)
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 2232e56ba1)
(cherry picked from commit c188f3367d)

Co-authored-by: andy.lee <andy.lee@suse.com>
2025-04-17 14:50:31 +08:00
mergify[bot]andandy.lee b0609020cc fix sshkey is not added to cloud init user data immediately (#252)
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit e2daa1ea0c)

Co-authored-by: andy.lee <andy.lee@suse.com>
2025-04-17 14:50:10 +08:00
mergify[bot]andAndy Lee feca476e36 Fix error banner position in all dialogs (backport #226) (#250)
* Fix error banner position in all dialogs (#226)

* fix error banner position in all dialogs

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

* update HarvesterImageDownloader.vue

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit 25439aee65)

# Conflicts:
#	pkg/harvester/dialog/ConfirmExecutionDialog.vue
#	pkg/harvester/dialog/HarvesterImageDownloader.vue

* remove unneeded dialog components

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-04-17 14:46:30 +08:00
mergify[bot]andAndy Lee e6e36e94f1 fix image.repo to image.repository (#214) (#251)
Signed-off-by: Andy Lee <andy.lee@suse.com>
(cherry picked from commit a24d01717a)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-04-17 14:46:11 +08:00
Andy Lee 54d48ce396 bump version to 1.0.6
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-17 13:47:13 +08:00
Andy Lee 404395c7d9 bump version to 1.0.5
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-08 15:26:18 +08:00
Andy Lee dc2437db99 bring status object in PUT dismiss API
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-08 15:12:13 +08:00
Andy Lee e8c8a54b9f add cleanForSave in harvester model
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-04-08 15:12:07 +08:00
andy.lee f3e4a8afe8 add pcideviceclaim and usbdeviceclaim models
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-04-08 15:11:55 +08:00
andy.lee e73c62739f fix unable to add/delete new hard disk
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-04-08 15:03:14 +08:00
Andy Lee 696af4e04f update yarn.lock
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-12 12:05:24 +08:00
Yi-Ya Chen 009f384a73 feat: fix $ctx redefine issue
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-03-12 12:04:24 +08:00
andy.lee 37766454bd move harvester schema.js into harvester folder
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-03-12 12:02:36 +08:00
Andy Lee 7943913ef7 replace tag format v1.5.0 to harvester-1.5.0 (#190)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-12 12:02:11 +08:00
Andy Lee 56a0fefd40 bump release version to v1.0.4
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-11 12:11:37 +08:00
Andy Lee ff1d5925a8 remove require dot for non-longhorn v1
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-11 12:09:18 +08:00
Andy Lee 63b5b980b7 Fix infobox style
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-03-11 12:09:10 +08:00
Yi-Ya Chen 1f7fd1246f refactor: default should selectable
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-03-11 12:07:52 +08:00
Yi-Ya Chen 34a25e9fc8 feat: gray out class name selector
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
2025-03-11 12:07:32 +08:00
Andy Lee fb8d81d953 bump version to v1.0.4-rc2 2025-03-11 12:06:29 +08:00
andy.lee ba810a52b2 add refreshIntervalInSecond in 1.4.2 feature flag
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-03-11 12:06:29 +08:00
andy.lee 3f4f30ba0b Add refreshIntervalInSeconds in backup targe edit page
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-03-11 12:06:29 +08:00
andy.lee b428aaecf8 bump version to v1.0.4-rc1
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-13 14:34:14 +08:00
andy.lee 6b07a03451 add v1.4.2 feature flag
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-02-13 14:19:37 +08:00
Volker Theile 2c756582e3 The hints on the branding page is using the wrong product name
The hints on the `Branding` page at `Advanced > Settings > UI > branding` are using the product name `Rancher` instead of `Harvester`.

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

Signed-off-by: Volker Theile <vtheile@suse.com>
(cherry picked from commit 849dcb6f26d52c955db669207f008de4480fe048)
2025-02-13 14:11:24 +08:00
149 changed files with 4466 additions and 9859 deletions
-6
View File
@@ -1,6 +0,0 @@
addAssignees: author
addReviewers: true
numberOfReviewers: 0
reviewers:
- a110605
- houhoucoop
-14
View File
@@ -2,7 +2,6 @@ pull_request_rules:
- name: Automatically open backport PR to release-harvester-v1.5 - name: Automatically open backport PR to release-harvester-v1.5
conditions: conditions:
- base=main - base=main
- label="require backport 1.5"
actions: actions:
backport: backport:
branches: branches:
@@ -12,19 +11,6 @@ pull_request_rules:
labels: labels:
- "v1.5 backport PR" - "v1.5 backport PR"
- 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 - name: Ask to resolve conflict
conditions: conditions:
- conflict - conflict
+6 -1
View File
@@ -2,13 +2,18 @@
### Summary ### Summary
### PR Checklists ### PR Checklists
- Do we need to backport this PR change to the [Harvester Dashboard](https://github.com/harvester/dashboard)?
- [ ] Yes, the relevant PR is at:
- Are backend engineers aware of UI changes? - Are backend engineers aware of UI changes?
- [ ] Yes, the backend owner is: - [ ] Yes, the backend owner is:
### Related Issue # ### Related Issue #
<!-- Define findings related to the feature or bug issue. --> <!-- Define findings related to the feature or bug issue. -->
### Test screenshot or video ### Test screenshot/video
<!-- Attach screenshot or video of the changes and eventual comparison if you find it necessary --> <!-- Attach screenshot or video of the changes and eventual comparison if you find it necessary -->
### Extra technical notes summary
<!-- Outline technical changes which may pass unobserved or may help to understand the process of solving the issue -->
-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
-46
View File
@@ -1,46 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":semanticCommitTypeAll(deps)",
":semanticCommitScopeDisabled"
],
"baseBranches": [
"main",
"/^release-harvester-v\\d+\\.\\d+$/"
],
"automergeMajor": false,
"semanticCommits": "enabled",
"semanticCommitType": "deps",
"prHourlyLimit": 12,
"timezone": "Asia/Taipei",
"schedule": ["after 10am on sunday"],
"postUpdateOptions": ["yarnDedupeFewer"],
"packageRules": [
{
"matchUpdateTypes": ["major"],
"enabled": false
},
{
"matchPackagePatterns": ["vue", "vue-router", "vuex"],
"matchUpdateTypes": ["major"],
"enabled": false
},
{
"matchPackageNames": ["@rancher/shell"],
"enabled": false
},
{
"matchUpdateTypes": ["minor"],
"groupName": "minor dependencies",
"labels": ["minor-update"],
"reviewers": ["a110605", "houhoucoop"]
},
{
"matchUpdateTypes": ["patch", "digest"],
"automerge": true,
"groupName": "patch digest dependencies",
"labels": ["patch-update", "automerge"]
}
]
}
@@ -1,17 +0,0 @@
name: "[PR Management] Auto Assign Reviewer & Assignee"
on:
pull_request_target:
types: [opened, ready_for_review]
permissions:
pull-requests: write
jobs:
auto-assign:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: rancher/gh-issue-mgr/auto-assign-action@main
with:
configuration-path: .github/auto-assign-config.yaml
-94
View File
@@ -1,94 +0,0 @@
name: "[PR Management] Add Labels"
on:
pull_request_target:
types: [opened, reopened]
branches:
- main
- 'release-harvester-v*'
permissions:
pull-requests: write
jobs:
add-require-backport-label:
if: github.event.pull_request.draft == false &&
github.event.pull_request.base.ref == 'main'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.base_ref }}
- name: Fetch release branches and PR labels
id: fetch_info
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
repo="${{ github.repository }}"
pr_number=${{ github.event.pull_request.number }}
release_branches=$(gh api "repos/${repo}/branches" --paginate --jq '.[].name' | grep -E '^release-harvester-v[0-9]+\.[0-9]+$' || true)
if [[ -z "$release_branches" ]]; then
echo "should_label=false" >> "$GITHUB_OUTPUT"
exit 0
fi
latest_branch=$(echo "$release_branches" | sort -Vr | head -n1)
version="${latest_branch#release-harvester-v}"
release_tag="v${version}.0"
tags=$(gh api "repos/${repo}/releases" --paginate --jq '.[].tag_name')
if echo "$tags" | grep -Fxq "$release_tag"; then
echo "should_label=false" >> "$GITHUB_OUTPUT"
exit 0
fi
label="require backport/v${version}"
echo "should_label=true" >> "$GITHUB_OUTPUT"
echo "backport_label=$label" >> "$GITHUB_OUTPUT"
pr_labels=$(gh pr view "$pr_number" --repo "$repo" --json labels --jq '.labels[].name' || echo "")
pr_labels_csv=$(echo "$pr_labels" | tr '\n' ',' | sed 's/,$//')
echo "pr_labels=$pr_labels_csv" >> "$GITHUB_OUTPUT"
- name: Add label if needed
if: steps.fetch_info.outputs.should_label == 'true' && !contains(steps.fetch_info.outputs.pr_labels, steps.fetch_info.outputs.backport_label)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Adding label: ${{ steps.fetch_info.outputs.backport_label }}"
gh pr edit ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} \
--add-label "${{ steps.fetch_info.outputs.backport_label }}"
add-backport-label:
if: github.event.pull_request.draft == false &&
startsWith(github.event.pull_request.base.ref, 'release-harvester-v')
runs-on: ubuntu-latest
steps:
- name: Check conditions for backport label
id: check
run: |
IS_MERGIFY=$(echo '${{ github.event.pull_request.user.login }}' | grep -iq 'mergify' && echo true || echo false)
TARGET_BRANCH=${{ github.event.pull_request.base.ref }}
echo "IS_MERGIFY=$IS_MERGIFY" >> $GITHUB_OUTPUT
echo "TARGET_BRANCH=$TARGET_BRANCH" >> $GITHUB_OUTPUT
- name: Add label if needed
if: steps.check.outputs.IS_MERGIFY == 'true' && startsWith(steps.check.outputs.TARGET_BRANCH, 'release-harvester-v')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TARGET_BRANCH="${{ steps.check.outputs.TARGET_BRANCH }}"
version="${TARGET_BRANCH#release-harvester-v}"
label="backport/v${version}"
echo "Adding label $label"
gh pr edit ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} \
--add-label "$label"
@@ -11,19 +11,18 @@ defaults:
working-directory: ./ working-directory: ./
jobs: jobs:
check-version: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - uses: actions/checkout@v4
uses: actions/checkout@v4 with:
fetch-depth: 1
- name: Check package version - name: Run tests
env: uses: ./.github/actions/lint
TAG_VERSION: ${{ github.event.release.tag_name }}
run: ./scripts/check-version.sh $TAG_VERSION
build-and-push-extension-catalog: build-and-push-extension-catalog:
needs: check-version needs: lint
uses: ./.github/workflows/build-extension-catalog.yml uses: ./.github/workflows/build-extension-catalog.yml
permissions: permissions:
actions: write actions: write
@@ -4,7 +4,6 @@ on:
push: push:
branches: branches:
- 'release-harvester-v*' - 'release-harvester-v*'
- 'main'
jobs: jobs:
setup-target-branch: setup-target-branch:
@@ -18,17 +17,14 @@ jobs:
- name: Determine target branch - name: Determine target branch
id: get-version id: get-version
run: | run: |
if [[ "${{ github.ref_name }}" == "main" ]]; then if [[ "${{ github.ref_name }}" =~ ^release-harvester-v([0-9]+\.[0-9]+)$ ]]; then
TARGET_BRANCH="main-head"
elif [[ "${{ github.ref_name }}" =~ ^release-harvester-v([0-9]+\.[0-9]+)$ ]]; then
TARGET_BRANCH="v${BASH_REMATCH[1]}-head" TARGET_BRANCH="v${BASH_REMATCH[1]}-head"
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_ENV
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_OUTPUT
else else
echo "Error: invalid branch format." && exit 1 echo "Error: invalid branch format." && exit 1
fi fi
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_ENV
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_OUTPUT
- name: Ensure target branch exists - name: Ensure target branch exists
run: | run: |
git fetch --all git fetch --all
@@ -132,7 +128,7 @@ jobs:
git push origin ${{ needs.setup-target-branch.outputs.target_branch }} git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
- name: Run Helm chart releaser - name: Run Helm chart releaser
uses: helm/chart-releaser-action@v1.7.0 uses: helm/chart-releaser-action@v1.4.1
with: with:
charts_dir: ./charts charts_dir: ./charts
env: env:
@@ -22,27 +22,26 @@ jobs:
- name: Determine release tag - name: Determine release tag
id: determine_tag id: determine_tag
run: | run: |
if [[ "${{ github.event.release.tag_name }}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then if [[ "${{ github.ref_name }}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
RELEASE_TAG="harvester-${BASH_REMATCH[1]}" RELEASE_TAG="harvester-${BASH_REMATCH[1]}"
echo "${RELEASE_TAG}" echo "${RELEASE_TAG}"
echo "release_tag=${RELEASE_TAG}" >> $GITHUB_OUTPUT echo "release_tag=${RELEASE_TAG}" >> $GITHUB_OUTPUT
else else
echo "Error: invalid tag format." && exit 1 echo "Error: invalid tag format." && exit 1
fi fi
check-version: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - uses: actions/checkout@v4
uses: actions/checkout@v4 with:
- name: Check package version fetch-depth: 1
env:
TAG_VERSION: ${{ github.event.release.tag_name }}
run: ./scripts/check-version.sh $TAG_VERSION
- name: Run tests
uses: ./.github/actions/lint
build-extension-charts: build-extension-charts:
needs: needs:
- setup-release-tag - setup-release-tag
- check-version - lint
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
permissions: permissions:
actions: write actions: write
@@ -5,20 +5,14 @@ on:
- v[1-9].* - v[1-9].*
jobs: jobs:
check-version: build-validation:
runs-on: ubuntu-latest name: Build Test
steps: uses: ./.github/workflows/run-lint.yaml
- name: Checkout repository
uses: actions/checkout@v4
- name: Check package version
env:
TAG_VERSION: ${{github.ref_name}}
run: ./scripts/check-version.sh $TAG_VERSION
build: build:
name: Build and Upload Package name: Build and Upload Package
uses: ./.github/workflows/build-and-publish-standalone.yaml uses: ./.github/workflows/build-and-publish-standalone.yaml
needs: check-version needs:
- build-validation
permissions: permissions:
contents: read contents: read
packages: write packages: write
-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@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"
+1 -37
View File
@@ -7,7 +7,6 @@ on:
- main - main
- 'release-*' - 'release-*'
pull_request: pull_request:
types: [opened, reopened, edited, synchronize]
branches: branches:
- main - main
- 'release-*' - 'release-*'
@@ -18,42 +17,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 # Need full history for commit-lint fetch-depth: 1
- name: Run tests - name: Run tests
uses: ./.github/actions/lint 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: |
echo "GITHUB_EVENT_NAME=$GITHUB_EVENT_NAME"
echo "GITHUB_BASE_SHA=$GITHUB_BASE_SHA"
echo "GITHUB_HEAD_SHA=$GITHUB_HEAD_SHA"
echo "GITHUB_BEFORE=$GITHUB_BEFORE"
echo "GITHUB_AFTER=$GITHUB_AFTER"
if [ "$GITHUB_EVENT_NAME" = "pull_request" ] && [ -n "$GITHUB_BASE_SHA" ] && [ -n "$GITHUB_HEAD_SHA" ]; then
FROM="$GITHUB_BASE_SHA"
TO="$GITHUB_HEAD_SHA"
elif [ -n "$GITHUB_BEFORE" ] && [ -n "$GITHUB_AFTER" ]; then
FROM="$GITHUB_BEFORE"
TO="$GITHUB_AFTER"
else
echo "No valid commit range found, skipping commitlint."
exit 0
fi
echo "FROM=$FROM"
echo "TO=$TO"
npx commitlint --from "$FROM" --to "$TO" --verbose
env:
GITHUB_EVENT_NAME: ${{ github.event_name }}
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
GITHUB_BEFORE: ${{ github.event.before }}
GITHUB_AFTER: ${{ github.event.after }}
-1
View File
@@ -1 +0,0 @@
npx --no -- commitlint --edit $1
-1
View File
@@ -1 +0,0 @@
yarn lint
+40 -134
View File
@@ -1,170 +1,76 @@
# harvester-ui-extension # 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). Rancher Extension used in [rancher/dashboard](https://github.com/rancher/dashboard) for [Harvester](https://harvesterhci.io) Dashboard UI.
> **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. From Rancher 2.10.0, Harvester integration is provided by this new Harvester UI Extension.
`
## Installation ## Installation
For detailed installation instructions, please refer to the [official Harvester documentation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210). To install the Harvester UI Extension in Rancher 2.10.0 and newer:
- Go to 'Extensions' in the Rancher UI (click on the jigsaw icon in the side bar)
- From the three-dots menu in the upper right of the page, open the menu and select 'Manage Repositories'
- From the Repositories page, click 'Create'
- For name, enter 'harvester-ui-extension'
- For Target, choose 'Git repository'
- For Git Repo URL, enter `https://github.com/harvester/harvester-ui-extension`
- For Git Branch, enter `gh-pages`
- Click 'Create`
- From the Repositories list, wait until the new Repository added updates to the 'Active' state
- Go back to 'Extensions'
- Click on the 'Available' tab if not already selected
- Click on 'Install' for the Harvester UI Extension
## Development Setup ## Running for Development
This is what you probably want to get started.
### Prerequisite
Nodejs version >= 20
Ensure **Node.js v20 or later** is installed for development and debugging.
### Standalone Mode ### Standalone Mode
Run the extension standalone with hot reload at `https://localhost:8005`. For development, serve with hot reload at https://localhost:8005
```bash ```bash
# Install dependencies # Install dependencies
yarn install yarn install
# Start the development server # Use RANCHER_ENV to specify the endpoint for your Harvester API
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
# Example with specific server version # Use VUE_APP_SERVER_VERSION to specify the Harvester server version
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.5.0 API=https://192.168.1.123 yarn dev # in case your are running a dev build of Harvester
``` RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=your-harvester-server-version API=https://your-harvester-ip yarn dev
You may also define environment variables in a `.env` file: # or put your env variables into a .env file
```env # Example
RANCHER_ENV=harvester RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.4.0 API=https://192.168.1.123 yarn dev
VUE_APP_SERVER_VERSION=v1.5.0
API=https://192.168.1.123
``` ```
### Rancher Integration Mode ### 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). In this mode, you need to have Rancher and run Harvester UI Extension as Rancher extension, see more detail in [Rancher UI extension](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
```bash ```bash
API=https://your-rancher-ip yarn dev API=https://your-rancher-ip yarn dev
# Example
API=https://192.168.1.123 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`
- Test `1.6.x` series → `v1.6-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 ## 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. For developers, after reading through the introduction on this page, head over to our [Getting Started](https://extensions.rancher.io/extensions/next/extensions-getting-started) guide to learn more.
## License License
=======
Copyright (c) 2014-2025 [SUSE, LLC.](https://www.suse.com/) Copyright (c) 2014-2023 [Rancher Labs, Inc.](http://rancher.com)
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
-43
View File
@@ -1,43 +0,0 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
// Enforce conventional commit format
'type-enum': [
2,
'always',
[
'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
]
],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'subject-case': [0, 'never'],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
'subject-max-length': [0, 'never'],
'body-leading-blank': [2, 'always'],
'body-max-line-length': [2, 'always', 100],
'footer-leading-blank': [2, 'always'],
'footer-max-line-length': [2, 'always', 100],
},
// Ignore merge commits and revert commits
ignores: [
(commit) => commit.includes('Merge'),
(commit) => commit.includes('Revert'),
(commit) => commit.includes('merge'),
(commit) => commit.includes('revert'),
],
};
+13 -19
View File
@@ -1,36 +1,36 @@
{ {
"name": "harvester-ui-extension", "name": "harvester-ui-extension",
"version": "1.6.1-rc1", "version": "1.0.6",
"private": false, "private": false,
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
}, },
"dependencies": { "dependencies": {
"@babel/plugin-transform-class-static-block": "7.28.3", "@rancher/shell": "3.0.1-rc.4",
"@rancher/shell": "3.0.5-rc.8",
"cache-loader": "^4.1.0", "cache-loader": "^4.1.0",
"color": "4.2.3", "color": "4.2.3",
"ip": "2.0.1", "ip": "2.0.1",
"node-polyfill-webpack-plugin": "^3.0.0", "node-polyfill-webpack-plugin": "^3.0.0",
"vue-draggable-next": "^2.2.1", "vue-draggable-next": "^2.2.1",
"yaml": "^2.5.1" "yaml": "^2.5.1",
"@babel/plugin-transform-class-static-block": "7.26.0"
}, },
"resolutions": { "resolutions": {
"@types/node": "~20.19.0", "@types/node": "~20.10.0",
"cronstrue": "2.59.0", "cronstrue": "2.50.0",
"d3-color": "3.1.0", "d3-color": "3.1.0",
"ejs": "3.1.10", "ejs": "3.1.10",
"follow-redirects": "1.15.11", "follow-redirects": "1.15.6",
"glob": "7.2.3", "glob": "7.2.3",
"glob-parent": "6.0.2", "glob-parent": "6.0.2",
"json5": "2.2.3", "json5": "2.2.3",
"@types/lodash": "4.17.20", "@types/lodash": "4.17.5",
"merge": "2.1.1", "merge": "2.1.1",
"node-forge": "1.3.1", "node-forge": "1.3.1",
"nth-check": "2.1.1", "nth-check": "2.1.1",
"qs": "6.14.0", "qs": "6.11.1",
"roarr": "7.21.1", "roarr": "7.0.4",
"semver": "7.7.2", "semver": "7.5.4",
"@vue/cli-service/html-webpack-plugin": "^5.0.0" "@vue/cli-service/html-webpack-plugin": "^5.0.0"
}, },
"scripts": { "scripts": {
@@ -41,16 +41,10 @@
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh", "build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs", "serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish", "publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name", "parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name"
"commitlint": "commitlint --edit",
"prepare": "husky"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/load": "^19.8.1",
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^7.1.0", "eslint-plugin-promise": "^7.1.0"
"husky": "^9.1.7"
} }
} }
@@ -5,10 +5,8 @@ import { Checkbox } from '@components/Form/Checkbox';
import ModalWithCard from '@shell/components/ModalWithCard'; import ModalWithCard from '@shell/components/ModalWithCard';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { HCI } from '../types'; import { HCI } from '../types';
import UpgradeInfo from './UpgradeInfo'; import UpgradeInfo from './UpgradeInfo';
export default { export default {
name: 'HarvesterUpgrade', name: 'HarvesterUpgrade',
@@ -36,7 +34,6 @@ export default {
selectMode: true, selectMode: true,
version: '', version: '',
enableLogging: true, enableLogging: true,
skipSingleReplicaDetachedVol: false,
readyReleaseNote: false, readyReleaseNote: false,
isOpen: false isOpen: false
}; };
@@ -71,10 +68,6 @@ export default {
return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG); return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG);
}, },
skipSingleReplicaDetachedVolFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
},
releaseLink() { releaseLink() {
return `https://github.com/harvester/harvester/releases/tag/${ this.version }`; return `https://github.com/harvester/harvester/releases/tag/${ this.version }`;
} }
@@ -111,11 +104,6 @@ export default {
spec: { version: this.version } spec: { version: this.version }
}; };
if (this.skipSingleReplicaDetachedVolFeatureEnabled && this.skipSingleReplicaDetachedVol) {
upgradeValue.metadata.annotations =
{ [HCI_ANNOTATIONS.SKIP_SINGLE_REPLICA_DETACHED_VOL]: JSON.stringify(this.skipSingleReplicaDetachedVol) };
}
if (this.canEnableLogging) { if (this.canEnableLogging) {
upgradeValue.spec.logEnabled = this.enableLogging; upgradeValue.spec.logEnabled = this.enableLogging;
} }
@@ -202,21 +190,6 @@ export default {
/> />
</div> </div>
<div
v-if="skipSingleReplicaDetachedVolFeatureEnabled"
class="mb-5"
>
<Checkbox
v-model:value="skipSingleReplicaDetachedVol"
class="check"
type="checkbox"
:label="t('harvester.upgradePage.skipSingleReplicaDetachedVol')"
/>
</div>
<hr
v-if="version"
class="divider"
/>
<div v-if="version"> <div v-if="version">
<p <p
v-clean-html="t('harvester.upgradePage.releaseTip', {url: releaseLink}, true)" v-clean-html="t('harvester.upgradePage.releaseTip', {url: releaseLink}, true)"
+25 -126
View File
@@ -1,11 +1,8 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import ActionMenu from '@shell/components/ActionMenuShell';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton'; import AsyncButton from '@shell/components/AsyncButton';
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings'; import { HCI_ALLOWED_SETTINGS, HCI_SETTING } from '../config/settings';
import { DOC } from '../config/doc-links';
import { docLink } from '../utils/feature-flags';
const CATEGORY = { const CATEGORY = {
ui: [ ui: [
@@ -22,7 +19,6 @@ export default {
components: { components: {
AsyncButton, AsyncButton,
Banner, Banner,
ActionMenu
}, },
props: { props: {
@@ -34,23 +30,15 @@ export default {
category: { category: {
type: String, type: String,
required: true, required: true,
},
searchQuery: {
type: String,
default: ''
} }
}, },
data() { data() {
const categorySettings = this.filterCategorySettings(); const categorySettings = this.filterCategorySettings();
const filteredSettings = this.filterSearchSettings(categorySettings, this.searchQuery);
return { return {
HCI_SETTING, HCI_SETTING,
categorySettings, categorySettings,
filteredSettings,
originalHideMap: this.createHideMap(categorySettings)
}; };
}, },
@@ -60,85 +48,12 @@ export default {
settings: { settings: {
deep: true, deep: true,
handler() { handler() {
this.categorySettings = this.filterCategorySettings(); this['categorySettings'] = this.filterCategorySettings();
this.filteredSettings = this.filterSearchSettings(this.categorySettings, this.searchQuery);
}
},
searchQuery: {
immediate: true,
handler(newQuery) {
const filtered = this.filterSearchSettings(this.categorySettings, newQuery);
this.filteredSettings = newQuery ? this.openJsonSettings(filtered) : filtered.map((s) => ({ ...s, hide: this.originalHideMap[s.id] ?? false }));
} }
} }
}, },
methods: { methods: {
createHideMap(settings = []) {
const map = settings.reduce((acc, s) => {
acc[s.id] = s.hide ?? false;
return acc;
}, {} );
return map;
},
filterSearchSettings(settings, searchKey) {
if (!searchKey) {
return this.filterCategorySettings();
}
const searchQuery = searchKey.toLowerCase();
return settings.filter((setting) => {
const id = setting.id?.toLowerCase() || '';
// filter by id
if (id.includes(searchQuery) ) {
return true;
}
let description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || '';
// remove <a> tags from description
if (description.includes('<a')) {
description = description.replace(/<a[^>]*>(.*?)<\/a>/g, '$1');
}
// filter by description
if (description.includes(searchQuery)) {
return true;
}
// filter by customized value
if (setting.customized === true && setting.data?.value) {
const value = setting.data.value?.toLowerCase() || '';
return value.includes(searchQuery);
}
// filter by json value
if (setting.kind === 'json' && setting.json) {
try {
const json = JSON.parse(setting.json);
const jsonString = JSON.stringify(json).toLowerCase();
return jsonString.includes(searchQuery);
} catch (e) {
console.error(`${ setting.id }: wrong format`, e); // eslint-disable-line no-console
return false;
}
}
// filter by default value
if (setting.data?.default) {
return setting.data?.default.includes(searchQuery);
}
return false;
});
},
filterCategorySettings() { filterCategorySettings() {
return this.settings.filter((s) => { return this.settings.filter((s) => {
if (!this.getFeatureEnabled(s.featureFlag)) { if (!this.getFeatureEnabled(s.featureFlag)) {
@@ -159,21 +74,25 @@ export default {
return id ? this.$store.getters['harvester-common/getFeatureEnabled'](id) : true; return id ? this.$store.getters['harvester-common/getFeatureEnabled'](id) : true;
}, },
showActionMenu(e, setting) {
const actionElement = e.srcElement;
this.$store.commit(`action-menu/show`, {
resources: setting.data,
elem: actionElement
});
},
getSettingOption(id) { getSettingOption(id) {
return HCI_ALLOWED_SETTINGS.find((setting) => setting.id === id); return HCI_ALLOWED_SETTINGS.find((setting) => setting.id === id);
}, },
openJsonSettings(settings) {
return settings.map((s) => s.hide ? { ...s, hide: false } : s);
},
toggleHide(s) { toggleHide(s) {
const setting = this.filteredSettings.find((setting) => setting.id === s.id); this.categorySettings.find((setting) => {
if (setting.id === s.id) {
if (setting) {
setting.hide = !setting.hide; setting.hide = !setting.hide;
this.originalHideMap[setting.id] = setting.hide;
} }
});
}, },
async testConnect(buttonDone, value) { async testConnect(buttonDone, value) {
@@ -199,19 +118,6 @@ export default {
} }
buttonDone(false); buttonDone(false);
} }
},
getDocLinkParams(setting) {
const settingConfig = HCI_ALLOWED_SETTINGS[setting.id] || HCI_SINGLE_CLUSTER_ALLOWED_SETTING[setting.id];
if (settingConfig?.docPath) {
const version = this.$store.getters['harvester-common/getServerVersion']();
const url = docLink(DOC[settingConfig.docPath], version);
return { url };
}
return {};
} }
}, },
}; };
@@ -220,7 +126,7 @@ export default {
<template> <template>
<div> <div>
<div <div
v-for="(setting, i) in filteredSettings" v-for="(setting, i) in categorySettings"
:key="i" :key="i"
class="advanced-setting mb-20" class="advanced-setting mb-20"
> >
@@ -242,7 +148,7 @@ export default {
Experimental Experimental
</span> </span>
</h1> </h1>
<h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)"> <h2 v-clean-html="t(setting.description, {}, true)">
</h2> </h2>
</div> </div>
<div <div
@@ -250,12 +156,15 @@ export default {
:id="setting.id" :id="setting.id"
class="action" class="action"
> >
<ActionMenu <button
:resource="setting.data" aria-haspopup="true"
:button-aria-label="t('advancedSettings.edit.label')" aria-expanded="false"
data-testid="action-button" type="button"
button-role="tertiary" class="btn btn-sm role-multi-action actions"
/> @click="showActionMenu($event, setting)"
>
<i class="icon icon-actions" />
</button>
</div> </div>
</div> </div>
<div value> <div value>
@@ -312,12 +221,6 @@ export default {
{{ setting.data.errMessage }} {{ setting.data.errMessage }}
</Banner> </Banner>
</div> </div>
<div
v-if="filteredSettings.length === 0"
class="advanced-setting mb-20 no-search-match"
>
<p> {{ t('harvester.setting.noSearchMatch') }} </p>
</div>
</div> </div>
</template> </template>
@@ -368,8 +271,4 @@ export default {
padding: 2px 10px; padding: 2px 10px;
font-size: 12px; font-size: 12px;
} }
.no-search-match {
text-align: center;
}
</style> </style>
+1 -1
View File
@@ -43,7 +43,7 @@ export default {
{{ t('harvester.upgradePage.upgradeInfo.tip') }} {{ t('harvester.upgradePage.upgradeInfo.tip') }}
</p> </p>
<p> <p class="mb-5">
{{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a {{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a
:href="releaseVersion" :href="releaseVersion"
target="_blank" target="_blank"
@@ -258,7 +258,7 @@ export default {
}, },
softReboot() { softReboot() {
this.vmResource.doSoftReboot(); this.vmResource.softrebootVM();
}, },
showKeysModal() { showKeysModal() {
@@ -372,12 +372,3 @@ export default {
background: rgb(40, 40, 40); background: rgb(40, 40, 40);
} }
</style> </style>
<style lang="scss">
.vm-console .v-popper__arrow-container {
display: none;
}
.vm-console .v-popper__popper{
margin-top: 8px;
}
</style>
@@ -174,6 +174,10 @@ export default {
return driver === LONGHORN_DRIVER || this.allowEmptySnapshotClassNameFeatureEnabled; return driver === LONGHORN_DRIVER || this.allowEmptySnapshotClassNameFeatureEnabled;
}, },
isBackupVolumeSnapshotClassNameSelectable(driver) {
return driver === LONGHORN_DRIVER;
},
add() { add() {
this.configArr.push({ this.configArr.push({
key: '', key: '',
@@ -243,6 +247,7 @@ export default {
:options="getVolumeSnapshotOptions(driver.key)" :options="getVolumeSnapshotOptions(driver.key)"
:required="isBackupVolumeSnapshotRequired(driver.key)" :required="isBackupVolumeSnapshotRequired(driver.key)"
:label="t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName')" :label="t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName')"
@selectable="isBackupVolumeSnapshotClassNameSelectable(driver.key)"
@keydown.native.enter.prevent="()=>{}" @keydown.native.enter.prevent="()=>{}"
@update:value="update" @update:value="update"
/> />
@@ -1,294 +0,0 @@
<script>
import { _EDIT } from '@shell/config/query-params';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import InfoBox from '@shell/components/InfoBox';
import { Banner } from '@components/Banner';
import { allHash } from '@shell/utils/promise';
import { CSI_DRIVER } from '../../types';
import { LONGHORN_DRIVER } from '@shell/config/types';
export default {
name: 'CSIOnlineExpandValidation',
components: {
Banner,
InfoBox,
LabeledSelect,
},
props: {
mode: {
type: String,
default: _EDIT,
},
value: {
type: Object,
default: () => ({}),
},
registerBeforeHook: {
type: Function,
required: true,
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
try {
await allHash({ csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }) });
this.fetchError = null;
} catch (error) {
console.error('Failed to fetch CSI drivers:', error); // eslint-disable-line no-console
this.fetchError = this.t(
'harvester.setting.csiOnlineExpandValidation.failedToLoadDrivers',
{ error: error.message || error },
true
);
}
},
data() {
return {
configArr: [],
parseError: null,
fetchError: null,
};
},
created() {
const initValue = this.value.value || this.value.default || '{}';
this.configArr = this.parseValue(initValue);
this.registerBeforeHook?.(this.willSave, 'willSave');
},
computed: {
allErrors() {
const errors = [];
if (this.fetchError) {
errors.push(this.fetchError);
}
if (this.parseError) {
errors.push(this.parseError);
}
return errors;
},
csiDrivers() {
if (this.fetchError) return [];
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || [];
},
provisioners() {
const usedKeys = this.configArr.map(({ key }) => key);
return this.csiDrivers
.filter(({ name }) => !usedKeys.includes(name))
.map(({ name }) => name);
},
provisionerValue() {
return [
{ label: 'True', value: true },
{ label: 'False', value: false },
];
},
disableAdd() {
return this.parseError || this.fetchError || this.configArr.length >= this.csiDrivers.length;
},
disableConfigEditing() {
return this.parseError || this.fetchError;
}
},
watch: {
'value.value'(newVal, oldVal) {
if (newVal !== oldVal) {
this.configArr = this.parseValue(newVal || '{}');
}
}
},
methods: {
_convertToBoolean(value) {
if (typeof value === 'boolean') return value;
if (typeof value === 'string') {
const lowerCaseValue = value.toLowerCase();
if (lowerCaseValue === 'true') return true;
if (lowerCaseValue === 'false') return false;
}
return false; // default to false for any other string or non-boolean type
},
parseValue(raw) {
try {
const json = JSON.parse(raw);
this.parseError = null;
return Object.entries(json).map(([key, value]) => ({
key,
value: this._convertToBoolean(value),
}));
} catch (e) {
console.error('[CSIOnlineExpandValidation] JSON Parsing Error:', raw, e); // eslint-disable-line no-console
this.parseError = this.t(
'harvester.setting.csiOnlineExpandValidation.invalidJsonFormat',
{ error: e.message },
true
);
return [];
}
},
stringifyConfig() {
const obj = {};
this.configArr.forEach(({ key, value }) => {
obj[key] = value;
});
return this.configArr.length ? JSON.stringify(obj) : '';
},
update() {
this.value.value = this.stringifyConfig();
},
willSave() {
const errors = [];
this.configArr.forEach(({ key }) => {
if (!key) {
errors.push(
this.t('validation.required', { key: this.t('harvester.setting.csiOnlineExpandValidation.provisioner') }, true)
);
}
});
this.value.value = this.stringifyConfig();
return errors.length ? Promise.reject(errors) : Promise.resolve();
},
useDefault() {
this.configArr = this.parseValue(this.value.default || '{}');
this.update();
},
disableEdit(driverKey) {
return this.fetchError || driverKey === LONGHORN_DRIVER;
},
add() {
if (this.disableConfigEditing) return;
this.configArr.push({ key: '', value: true });
},
remove(index) {
if (this.disableConfigEditing) return;
this.configArr.splice(index, 1);
this.update();
},
onValueChange(idx, newVal) {
if (this.disableConfigEditing) return;
const val = newVal === 'true' ? true : newVal === 'false' ? false : newVal;
this.configArr[idx].value = val;
this.update();
},
},
};
</script>
<template>
<div>
<Banner
v-for="(errorMsg, index) in allErrors"
:key="index"
color="error"
>
{{ errorMsg }}
</Banner>
<InfoBox
v-for="(driver, idx) in configArr"
:key="idx"
class="box"
>
<button
class="role-link btn btn-sm remove"
type="button"
:disabled="disableEdit(driver.key)"
@click="remove(idx)"
>
<i class="icon icon-x" />
</button>
<div class="row">
<div class="col span-4">
<LabeledSelect
v-model:value="driver.key"
label-key="harvester.setting.csiOnlineExpandValidation.provisioner"
required
searchable
:mode="mode"
:disabled="disableEdit(driver.key)"
:options="provisioners"
@update:value="update"
@keydown.native.enter.prevent
/>
</div>
<div class="col span-4">
<LabeledSelect
v-model:value="driver.value"
:value="driver.value.toString()"
label-key="harvester.setting.csiOnlineExpandValidation.value"
required
searchable
:mode="mode"
:disabled="disableEdit(driver.key)"
:options="provisionerValue"
@update:value="val => onValueChange(idx, val)"
@keydown.native.enter.prevent
/>
</div>
</div>
</InfoBox>
<button
class="btn btn-sm role-primary"
:disabled="disableAdd"
@click="add"
>
{{ t('generic.add') }}
</button>
</div>
</template>
<style lang="scss" scoped>
.box {
position: relative;
padding-top: 40px;
}
.remove {
position: absolute;
top: 10px;
right: 10px;
padding: 0;
}
</style>
@@ -1,5 +1,6 @@
<script> <script>
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
@@ -61,7 +62,7 @@ export default {
<div <div
class="row" class="row"
@input="update" @update:value="update"
> >
<div class="col span-12"> <div class="col span-12">
<LabeledInput <LabeledInput
@@ -1,222 +0,0 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import { RadioGroup } from '@components/Form/Radio';
import { SECRET } from '@shell/config/types';
import { exceptionToErrorsArray } from '@shell/utils/error';
import FileSelector, { createOnSelected } from '@shell/components/form/FileSelector';
import YamlEditor from '@shell/components/YamlEditor';
export default {
name: 'HarvesterRancherCluster',
components: {
RadioGroup,
FileSelector,
YamlEditor
},
mixins: [CreateEditView],
props: {
value: {
type: Object,
default: () => ({}),
},
registerBeforeHook: {
type: Function,
required: false,
default: () => {},
},
},
data() {
let parseDefaultValue = {};
try {
const data = this.value.value || this.value.default || '{}';
const parsed = JSON.parse(data);
parseDefaultValue = {
kubeConfig: '',
removeUpstreamClusterWhenNamespaceIsDeleted: parsed.removeUpstreamClusterWhenNamespaceIsDeleted || false
};
} catch (error) {
parseDefaultValue = {
kubeConfig: '',
removeUpstreamClusterWhenNamespaceIsDeleted: false
};
}
return {
parseDefaultValue,
errors: [],
existingSecret: null,
};
},
async created() {
await this.checkExistingSecret();
if (this.registerBeforeHook) {
this.registerBeforeHook(this.willSave, 'willSave');
}
},
methods: {
onKeySelected: createOnSelected('parseDefaultValue.kubeConfig'),
update() {
if (this.parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted) {
this.value['value'] = JSON.stringify({ removeUpstreamClusterWhenNamespaceIsDeleted: true });
} else {
this.value['value'] = this.value.default || '{}';
}
},
async checkExistingSecret() {
const inStore = this.$store.getters['currentProduct'].inStore;
await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
const secrets = this.$store.getters[`${ inStore }/all`](SECRET) || [];
this.existingSecret = secrets.find((secret) => secret.metadata.name === 'rancher-cluster-config' &&
secret.metadata.namespace === 'harvester-system'
);
// If the secret exists and has data, populate the kubeConfig
if (this.existingSecret?.data?.kubeConfig) {
const decodedContent = atob(this.existingSecret.data.kubeConfig);
this.parseDefaultValue.kubeConfig = decodedContent;
this.$nextTick(() => {
this.update();
});
}
},
async createOrUpdateRancherKubeConfigSecret() {
this.errors = [];
// Check if kubeConfig is provided
if (!this.parseDefaultValue.kubeConfig) {
this.errors.push(this.t('validation.required', { key: this.t('harvester.setting.rancherCluster.kubeConfig') }, true));
return Promise.reject(this.errors);
}
try {
let secret;
if (this.existingSecret) {
secret = this.existingSecret;
secret.setData('kubeConfig', this.parseDefaultValue.kubeConfig);
} else {
const inStore = this.$store.getters['currentProduct'].inStore;
secret = await this.$store.dispatch(`${ inStore }/create`, {
apiVersion: 'v1',
kind: 'Secret',
metadata: {
name: 'rancher-cluster-config',
namespace: 'harvester-system'
},
type: 'secret',
data: { kubeConfig: btoa(this.parseDefaultValue.kubeConfig) }
});
}
await secret.save();
return Promise.resolve();
} catch (err) {
this.errors = exceptionToErrorsArray(err);
return Promise.reject(this.errors);
}
},
async deleteRancherKubeConfigSecret() {
if (this.existingSecret) {
this.existingSecret.remove();
}
},
async willSave() {
// Only create or update secret if enabled
if (this.parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted) {
await this.createOrUpdateRancherKubeConfigSecret();
} else {
await this.deleteRancherKubeConfigSecret();
}
return Promise.resolve();
},
useDefault() {
this.parseDefaultValue = {
kubeConfig: '',
removeUpstreamClusterWhenNamespaceIsDeleted: false
};
}
},
watch: {
'parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted'(val, oldVal) {
if (val && !oldVal && this.existingSecret?.data?.kubeConfig) {
// Populate kubeConfig with the existing secret value
this.parseDefaultValue.kubeConfig = atob(this.existingSecret.data.kubeConfig);
}
},
'parseDefaultValue.kubeConfig'(val) {
this.$refs.yaml?.updateValue(val);
}
}
};
</script>
<template>
<div>
<div class="row mt-20">
<div class="col span-12">
<RadioGroup
v-model:value="parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted"
:label="t('harvester.setting.rancherCluster.removeUpstreamClusterWhenNamespaceIsDeleted')"
name="removeUpstreamClusterWhenNamespaceIsDeleted"
:options="[true, false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
@update:value="update"
/>
</div>
</div>
<div
v-if="parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted"
class="row mt-20"
>
<div class="col span-12">
<FileSelector
class="btn btn-sm bg-primary mb-10"
:label="t('generic.readFromFile')"
@selected="onKeySelected"
/>
<YamlEditor
ref="yaml"
v-model:value="parseDefaultValue.kubeConfig"
class="yaml-editor"
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
@update:value="update"
/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
$yaml-height: 540px;
:deep() .yaml-editor{
flex: 1;
min-height: $yaml-height;
& .code-mirror .CodeMirror {
position: initial;
height: auto;
min-height: $yaml-height;
}
}
</style>
@@ -12,9 +12,6 @@ import { NODE } from '@shell/config/types';
import { HCI } from '../../types'; import { HCI } from '../../types';
import { DOC } from '../../config/doc-links'; import { DOC } from '../../config/doc-links';
import { docLink } from '../../utils/feature-flags'; import { docLink } from '../../utils/feature-flags';
import { NETWORK_TYPE } from '../../config/types';
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
export default { export default {
name: 'HarvesterEditStorageNetwork', name: 'HarvesterEditStorageNetwork',
@@ -60,14 +57,11 @@ export default {
data() { data() {
let parsedDefaultValue = {}; let parsedDefaultValue = {};
let openVlan = false; let openVlan = false;
let networkType = L2VLAN;
try { try {
parsedDefaultValue = JSON.parse(this.value.value); parsedDefaultValue = JSON.parse(this.value.value);
networkType = 'vlan' in parsedDefaultValue ? L2VLAN : UNTAGGED; // backend doesn't provide networkType, so we check if vlan is provided instead
openVlan = true; openVlan = true;
} catch (error) { } catch (error) {
networkType = L2VLAN;
parsedDefaultValue = { parsedDefaultValue = {
vlan: '', vlan: '',
clusterNetwork: '', clusterNetwork: '',
@@ -79,7 +73,6 @@ export default {
return { return {
openVlan, openVlan,
networkType,
errors: [], errors: [],
exclude, exclude,
parsedDefaultValue, parsedDefaultValue,
@@ -94,35 +87,16 @@ export default {
}, },
computed: { computed: {
showVlan() {
return this.networkType === L2VLAN;
},
networkTypes() {
const types = [L2VLAN];
if (this.untaggedNetworkSettingEnabled) {
types.push(UNTAGGED);
}
return types;
},
storageNetworkExampleLink() { storageNetworkExampleLink() {
const version = this.$store.getters['harvester-common/getServerVersion'](); const version = this.$store.getters['harvester-common/getServerVersion']();
return docLink(DOC.STORAGE_NETWORK_EXAMPLE, version); return docLink(DOC.STORAGE_NETWORK_EXAMPLE, version);
}, },
untaggedNetworkSettingEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('untaggedNetworkSetting');
},
clusterNetworkOptions() { clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || []; const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
// untaggedNetwork filter out mgmt cluster network
const clusterNetworksOptions = this.networkType === UNTAGGED ? clusterNetworks.filter((net) => net.id !== 'mgmt') : clusterNetworks;
return clusterNetworksOptions.map((n) => { return clusterNetworks.map((n) => {
const disabled = !n.isReadyForStorageNetwork; const disabled = !n.isReadyForStorageNetwork;
return { return {
@@ -134,50 +108,7 @@ export default {
}, },
}, },
watch: {
networkType: {
handler(neu) {
this.parsedDefaultValue.clusterNetwork = '';
if (neu === L2VLAN) {
this.parsedDefaultValue.vlan = '';
} else {
delete this.parsedDefaultValue.vlan;
}
},
deep: true
}
},
methods: { methods: {
inputVlan(neu) {
if (neu === '') {
this.parsedDefaultValue.vlan = '';
return;
}
const newValue = Number(neu);
if (newValue > 4094) {
this.parsedDefaultValue.vlan = 4094;
} else if (newValue < 1) {
this.parsedDefaultValue.vlan = 1;
} else {
this.parsedDefaultValue.vlan = newValue;
}
},
useDefault() {
this.openVlan = false;
this.networkType = L2VLAN;
this.parsedDefaultValue = {
vlan: '',
clusterNetwork: '',
range: '',
exclude: []
};
},
update() { update() {
const exclude = this.exclude.filter((ip) => ip); const exclude = this.exclude.filter((ip) => ip);
@@ -207,7 +138,7 @@ export default {
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true)); errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true));
} }
if (this.networkType === L2VLAN && !this.parsedDefaultValue.vlan) { if (!this.parsedDefaultValue.vlan) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true)); errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
} }
@@ -260,25 +191,13 @@ export default {
/> />
<div v-if="openVlan"> <div v-if="openVlan">
<LabeledSelect
v-model:value="networkType"
class="mb-20"
:options="networkTypes"
:mode="mode"
:label="t('harvester.fields.type')"
required
/>
<LabeledInput <LabeledInput
v-if="showVlan"
v-model:value.number="parsedDefaultValue.vlan" v-model:value.number="parsedDefaultValue.vlan"
type="number" type="number"
class="mb-20" class="mb-20"
:mode="mode" :mode="mode"
required required
placeholder="e.g. 1 - 4094"
label-key="harvester.setting.storageNetwork.vlan" label-key="harvester.setting.storageNetwork.vlan"
@update:value="inputVlan"
/> />
<LabeledSelect <LabeledSelect
@@ -3,9 +3,6 @@ import { NAMESPACE } from '@shell/config/types';
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
const SELECT_ALL = 'select_all';
const UNSELECT_ALL = 'unselect_all';
export default { export default {
name: 'HarvesterBundleNamespaces', name: 'HarvesterBundleNamespaces',
@@ -14,74 +11,47 @@ export default {
mixins: [CreateEditView], mixins: [CreateEditView],
async fetch() { async fetch() {
this.loading = true;
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE }); await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
try {
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
const response = await this.$store.dispatch('harvester/request', { url });
this.defaultNamespaces = response.data || [];
} catch (error) {
this.defaultNamespaces = [];
} finally {
this.loading = false;
}
}, },
data() { data() {
let namespaces = [];
const namespacesStr = this.value?.value || this.value?.default || ''; const namespacesStr = this.value?.value || this.value?.default || '';
const namespaces = namespacesStr ? namespacesStr.split(',') : [];
return { if (namespacesStr) {
namespaces, namespaces = namespacesStr.split(',');
defaultNamespaces: [], }
loading: true
}; return { namespaces };
}, },
computed: { computed: {
allNamespaces() {
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
},
filteredNamespaces() {
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
},
namespaceOptions() { namespaceOptions() {
const allSelected = return this.$store.getters['harvester/all'](NAMESPACE).map((N) => {
this.namespaces.length === this.filteredNamespaces.length && return {
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns)); label: N.id,
value: N.id
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL }; };
});
return [controlOption, ...this.filteredNamespaces];
} }
}, },
methods: { methods: {
update(selected) { update() {
if (selected.includes(SELECT_ALL)) { const namespaceStr = this.namespaces.join(',');
this.namespaces = [...this.filteredNamespaces];
} else if (selected.includes(UNSELECT_ALL)) {
this.namespaces = [];
} else {
this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL);
}
this.value.value = this.namespaces.join(','); this.value['value'] = namespaceStr;
} }
}, },
watch: { watch: {
'value.value'(newVal) { 'value.value': {
const raw = newVal || this.value.default || ''; handler(neu) {
if (neu === this.value.default || !neu) {
this.namespaces = raw ? raw.split(',') : []; this.namespaces = [];
}
},
deep: true
} }
} }
}; };
@@ -92,7 +62,6 @@ export default {
<div class="col span-12"> <div class="col span-12">
<LabeledSelect <LabeledSelect
v-model:value="namespaces" v-model:value="namespaces"
:loading="loading"
:multiple="true" :multiple="true"
label-key="nameNsDescription.namespace.label" label-key="nameNsDescription.namespace.label"
:mode="mode" :mode="mode"
@@ -1,163 +0,0 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { RadioGroup } from '@components/Form/Radio';
import { mapGetters } from 'vuex';
export default {
name: 'HarvesterUpgradeConfig',
components: {
LabeledInput,
LabeledSelect,
RadioGroup
},
mixins: [CreateEditView],
data() {
let parseDefaultValue = {};
try {
parseDefaultValue = this.value.value ? JSON.parse(this.value.value) : JSON.parse(this.value.default);
} catch (error) {
parseDefaultValue = JSON.parse(this.value.default);
}
parseDefaultValue = this.normalizeValue(parseDefaultValue);
return {
parseDefaultValue,
errors: []
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
strategyOptions() {
return [
{ value: 'sequential', label: 'sequential' },
{ value: 'skip', label: 'skip' },
{ value: 'parallel', label: 'parallel' }
];
}
},
created() {
this.update();
},
methods: {
normalizeValue(obj) {
if (!obj.imagePreloadOption) {
obj.imagePreloadOption = { strategy: { type: 'sequential' } };
}
if (!obj.imagePreloadOption.strategy) {
obj.imagePreloadOption.strategy = { type: 'sequential' };
}
if (!obj.imagePreloadOption.strategy.type) {
obj.imagePreloadOption.strategy.type = 'sequential';
}
// Only set concurrency if type is 'parallel'
if (obj.imagePreloadOption.strategy.type === 'parallel') {
if (typeof obj.imagePreloadOption.strategy.concurrency !== 'number') {
obj.imagePreloadOption.strategy.concurrency = 0;
}
} else {
delete obj.imagePreloadOption.strategy.concurrency;
}
if (typeof obj.restoreVM !== 'boolean') {
obj.restoreVM = false;
}
return obj;
},
update() {
try {
// Clone to avoid mutating the form state
const valueToSave = JSON.parse(JSON.stringify(this.parseDefaultValue));
if (valueToSave.imagePreloadOption && valueToSave.imagePreloadOption.strategy) {
if (valueToSave.imagePreloadOption.strategy.type !== 'parallel') {
delete valueToSave.imagePreloadOption.strategy.concurrency;
}
}
this.value['value'] = JSON.stringify(valueToSave, null, 2);
this.errors = [];
} catch (e) {
this.errors = ['Invalid JSON'];
}
}
},
watch: {
value: {
handler(neu) {
let parseDefaultValue;
try {
parseDefaultValue = JSON.parse(neu.value);
} catch (err) {
parseDefaultValue = JSON.parse(this.value.default);
}
parseDefaultValue = this.normalizeValue(parseDefaultValue);
this['parseDefaultValue'] = parseDefaultValue;
this.update();
},
deep: true
}
}
};
</script>
<template>
<div>
<div
class="row"
@input="update"
>
<div class="col span-12">
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.imagePreloadStrategy') }}</b></label>
<LabeledSelect
v-model:value="parseDefaultValue.imagePreloadOption.strategy.type"
class="mb-20"
:mode="mode"
:label="t('harvester.setting.upgrade.strategyType')"
:options="strategyOptions"
@update:value="update"
/>
<LabeledInput
v-if="parseDefaultValue.imagePreloadOption.strategy.type === 'parallel'"
v-model:value.number="parseDefaultValue.imagePreloadOption.strategy.concurrency"
class="mb-20"
:mode="mode"
:label="t('harvester.setting.upgrade.concurrency')"
min="0"
type="number"
/>
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.restoreVM') }}</b></label>
<RadioGroup
v-model:value="parseDefaultValue.restoreVM"
class="mb-20"
name="restoreVM"
:options="[true, false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
@update:value="update"
/>
<div
v-if="errors.length"
class="error"
>
{{ errors[0] }}
</div>
</div>
</div>
</div>
</template>
<style scoped>
.error {
color: #d9534f;
margin-top: 5px;
}
</style>
@@ -1,270 +0,0 @@
<script>
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
import { RadioGroup } from '@components/Form/Radio';
import { Banner } from '@components/Banner';
import ArrayList from '@shell/components/form/ArrayList';
import { allHash } from '@shell/utils/promise';
import { isValidCIDR } from '@shell/utils/validators/cidr';
import { NODE } from '@shell/config/types';
import { _EDIT } from '@shell/config/query-params';
import { HCI } from '../../types';
const DEFAULT_NETWORK = {
clusterNetwork: '',
vlan: '',
range: '',
exclude: [],
};
export default {
name: 'VMMigrationNetwork',
components: {
LabeledInput,
LabeledSelect,
RadioGroup,
Banner,
ArrayList,
},
props: {
registerBeforeHook: {
type: Function,
required: true,
},
mode: {
type: String,
default: _EDIT,
},
value: {
type: Object,
default: () => ({ value: '' }),
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
try {
await allHash({
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
});
this.fetchError = null;
} catch (e) {
console.error('Failed to fetch network data:', e); // eslint-disable-line no-console
this.fetchError = this.t('harvester.setting.vmMigrationNetwork.fetchError', { error: e.message || e }, true);
}
},
data() {
const { parsed, enabled, parseError } = this.parseInitialValue();
return {
enabled,
network: { ...DEFAULT_NETWORK, ...parsed },
fetchError: null,
parseError,
};
},
created() {
this.registerBeforeHook?.(this.willSave, 'willSave');
},
computed: {
allErrors() {
return [this.fetchError, this.parseError].filter(Boolean);
},
clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const networks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
return networks.map((net) => ({
label: net.isReadyForStorageNetwork ? net.id : `${ net.id } (${ this.t('generic.notReady') })`,
value: net.id,
disabled: !net.isReadyForStorageNetwork,
}));
},
disableEdit() {
return !!(this.fetchError || this.parseError);
},
},
methods: {
parseInitialValue() {
let parsed = {};
let enabled = false;
let parseError = null;
try {
if (typeof this.value.value === 'string' && this.value.value.trim()) {
parsed = JSON.parse(this.value.value);
enabled = true;
}
} catch (e) {
console.error('[VMMigrationNetwork] Failed to parse value:', e); // eslint-disable-line no-console
parseError = this.t('harvester.setting.vmMigrationNetwork.parseError', { error: e.message }, true);
}
if (!Array.isArray(parsed.exclude)) {
parsed.exclude = [];
}
return {
parsed, enabled, parseError
};
},
clearErrors() {
this.fetchError = null;
this.parseError = null;
},
inputVlan(val) {
this.network.vlan = val ? Math.min(4094, Math.max(1, Number(val))) : '';
this.update();
},
useDefault() {
this.network = { ...DEFAULT_NETWORK };
this.value.value = '';
this.enabled = false;
this.clearErrors();
},
update() {
try {
this.value.value = this.enabled ? JSON.stringify({
...this.network,
exclude: (this.network.exclude || []).filter((e) => !!e?.trim()),
}) : '';
} catch (e) {
console.error('Failed to stringify network config:', e); // eslint-disable-line no-console
this.value.value = '';
}
},
validateInputs() {
const errors = [];
if (!this.network.clusterNetwork) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.clusterNetwork') }, true));
}
if (!this.network.range) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.range.label') }, true));
} else if (!isValidCIDR(this.network.range)) {
errors.push(this.t('harvester.setting.vmMigrationNetwork.range.invalid', null, true));
}
if (this.network.vlan === '') {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.vlan') }, true));
} else {
const vlan = Number(this.network.vlan);
if (isNaN(vlan) || vlan < 1 || vlan > 4094) {
errors.push(this.t('validation.between', {
key: this.t('harvester.setting.vmMigrationNetwork.vlan'),
min: 1,
max: 4094,
}, true));
}
}
for (const cidr of this.network.exclude || []) {
if (cidr && !isValidCIDR(cidr)) {
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', { value: cidr }, true));
}
}
return errors;
},
async willSave() {
if (!this.enabled) {
this.useDefault();
return Promise.resolve();
}
this.update();
const errors = this.validateInputs();
return errors.length ? Promise.reject(errors) : Promise.resolve();
},
},
};
</script>
<template>
<div>
<Banner
v-for="(errorMsg, index) in allErrors"
:key="index"
color="error"
>
{{ errorMsg }}
</Banner>
<RadioGroup
v-model:value="enabled"
class="mb-20"
name="enableMigrationNetwork"
:options="[true, false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
@update:value="update"
/>
<template v-if="enabled">
<LabeledSelect
v-model:value="network.clusterNetwork"
required
label-key="harvester.setting.vmMigrationNetwork.clusterNetwork"
class="mb-20"
:mode="mode"
:options="clusterNetworkOptions"
:disabled="disableEdit"
@update:value="update"
/>
<LabeledInput
v-model:value.number="network.vlan"
required
type="number"
class="mb-20"
:min="1"
:max="4094"
:mode="mode"
placeholder="e.g. 1 - 4094"
label-key="harvester.setting.vmMigrationNetwork.vlan"
:disabled="disableEdit"
@update:value="inputVlan"
/>
<LabeledInput
v-model:value="network.range"
required
class="mb-5"
:mode="mode"
:placeholder="t('harvester.setting.vmMigrationNetwork.range.placeholder')"
label-key="harvester.setting.vmMigrationNetwork.range.label"
:disabled="disableEdit"
@update:value="update"
/>
<ArrayList
v-model:value="network.exclude"
:show-header="true"
:default-add-value="''"
:mode="mode"
:add-disabled="disableEdit"
:add-label="t('harvester.setting.vmMigrationNetwork.exclude.addButton')"
:value-label="t('harvester.setting.vmMigrationNetwork.exclude.label')"
:value-placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
@update:value="update"
/>
</template>
</div>
</template>
+2 -4
View File
@@ -2,9 +2,7 @@
export const DOC = { export const DOC = {
CONSOLE_URL: `/host/#remote-console`, CONSOLE_URL: `/host/#remote-console`,
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`, 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`, STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`, KSMTUNED_MODE: `/host/#ksmtuned-mode`,
UPGRADE_URL: `/upgrade/index`
}; };
+55 -54
View File
@@ -1,19 +1,31 @@
import semver from 'semver'; // https://github.com/harvester/dashboard/releases/tag/v1.3.0
const featuresV130 = [
const FEATURE_FLAGS = {
'v1.3.0': [
'supportHarvesterClusterVersion' 'supportHarvesterClusterVersion'
], ];
'v1.3.1': [
// https://github.com/harvester/dashboard/releases/tag/v1.3.1
const featuresV131 = [
...featuresV130,
'autoRotateRke2CertsSetting', 'autoRotateRke2CertsSetting',
'supportBundleNodeCollectionTimeoutSetting' 'supportBundleNodeCollectionTimeoutSetting'
], ];
'v1.3.2': [
// https://github.com/harvester/dashboard/releases/tag/v1.3.2
const featuresV132 = [
...featuresV131,
'kubeconfigDefaultTokenTTLMinutesSetting', 'kubeconfigDefaultTokenTTLMinutesSetting',
'improveMaintenanceMode', 'improveMaintenanceMode',
], ];
'v1.3.3': [],
'v1.4.0': [ // TODO: add v1.3.3 official release note
// https://github.com/harvester/dashboard/releases/tag/v1.3.3-dev-20250105
const featuresV133 = [
...featuresV132,
];
// https://github.com/harvester/dashboard/releases/tag/v1.4.0
const featuresV140 = [
...featuresV132,
'cpuPinning', 'cpuPinning',
'usbPassthrough', 'usbPassthrough',
'volumeEncryption', 'volumeEncryption',
@@ -21,49 +33,38 @@ const FEATURE_FLAGS = {
'vmSnapshotQuota', 'vmSnapshotQuota',
'longhornV2LVMSupport', 'longhornV2LVMSupport',
'improveMaintenanceMode', 'improveMaintenanceMode',
'upgradeConfigSetting' ];
],
'v1.4.1': [], // https://github.com/harvester/dashboard/releases/tag/v1.4.1
'v1.4.2': [ const featuresV141 = [
...featuresV140
];
// https://github.com/harvester/dashboard/releases/tag/v1.4.2
const featuresV142 = [
...featuresV141,
'refreshIntervalInSecond', 'refreshIntervalInSecond',
'allowEmptySnapshotClassName' 'allowEmptySnapshotClassName'
], ];
'v1.4.3': [],
'v1.5.0': [ // https://github.com/harvester/dashboard/releases/tag/v1.4.3-rc1
'tpmPersistentState', const featuresV143 = [
'efiPersistentState', ...featuresV142,
'untaggedNetworkSetting', ];
'skipSingleReplicaDetachedVol',
'thirdPartyStorage', // https://github.com/harvester/harvester-ui-extension/releases/tag/v1.5.0
'liveMigrationProgress' const featuresV150 = [
], ...featuresV142
'v1.5.1': [], ];
'v1.6.0': [
'vmMachineTypes', export const RELEASE_FEATURES = {
'customSupportBundle', 'v1.3.0': featuresV130,
'csiOnlineExpandValidation', 'v1.3.1': featuresV131,
'vmNetworkMigration', 'v1.3.2': featuresV132,
'kubeovnVpcSubnet', 'v1.3.3': featuresV133,
'rancherClusterSetting', 'v1.4.0': featuresV140,
'cpuMemoryHotplug', 'v1.4.1': featuresV141,
'cdiSettings', 'v1.4.2': featuresV142,
'vmCloneRunStrategy', 'v1.4.3': featuresV143,
], 'v1.5.0': featuresV150
'v1.6.1': [],
}; };
const generateFeatureFlags = () => {
const versions = [...Object.keys(FEATURE_FLAGS)].filter((version) => semver.valid(version)).sort(semver.compare);
const generatedFlags = {};
versions.forEach((version, index) => {
const previousVersion = versions[index - 1];
generatedFlags[version] = previousVersion ? [...generatedFlags[previousVersion], ...FEATURE_FLAGS[version]] : [...FEATURE_FLAGS[version]];
});
return generatedFlags;
};
export const RELEASE_FEATURES = generateFeatureFlags();
+2 -23
View File
@@ -24,7 +24,6 @@ import {
CONFIGURED_PROVIDERS, CONFIGURED_PROVIDERS,
SUB_TYPE, SUB_TYPE,
ADDRESS, ADDRESS,
DESCRIPTION,
} from '@shell/config/table-headers'; } from '@shell/config/table-headers';
import { IF_HAVE } from '@shell/store/type-map'; import { IF_HAVE } from '@shell/store/type-map';
import { import {
@@ -32,9 +31,7 @@ import {
FINGERPRINT, FINGERPRINT,
IMAGE_PROGRESS, IMAGE_PROGRESS,
SNAPSHOT_TARGET_VOLUME, SNAPSHOT_TARGET_VOLUME,
IMAGE_VIRTUAL_SIZE, IMAGE_VIRTUAL_SIZE
IMAGE_STORAGE_CLASS,
HARVESTER_DESCRIPTION
} from './table-headers'; } from './table-headers';
const TEMPLATE = HCI.VM_VERSION; const TEMPLATE = HCI.VM_VERSION;
@@ -224,8 +221,6 @@ export function init($plugin, store) {
STATE, STATE,
NAME_COL, NAME_COL,
NAMESPACE_COL, NAMESPACE_COL,
HARVESTER_DESCRIPTION,
IMAGE_STORAGE_CLASS,
IMAGE_PROGRESS, IMAGE_PROGRESS,
IMAGE_DOWNLOAD_SIZE, IMAGE_DOWNLOAD_SIZE,
IMAGE_VIRTUAL_SIZE, IMAGE_VIRTUAL_SIZE,
@@ -258,7 +253,7 @@ export function init($plugin, store) {
}); });
if (isSingleVirtualCluster) { if (isSingleVirtualCluster) {
headers(NAMESPACE, [STATE, NAME_UNLINKED, DESCRIPTION, AGE]); headers(NAMESPACE, [STATE, NAME_UNLINKED, AGE]);
basicType([NAMESPACE]); basicType([NAMESPACE]);
virtualType({ virtualType({
labelKey: 'harvester.namespace.label', labelKey: 'harvester.namespace.label',
@@ -424,7 +419,6 @@ export function init($plugin, store) {
[ [
HCI.CLUSTER_NETWORK, HCI.CLUSTER_NETWORK,
HCI.NETWORK_ATTACHMENT, HCI.NETWORK_ATTACHMENT,
HCI.VPC,
HCI.LB, HCI.LB,
HCI.IP_POOL, HCI.IP_POOL,
], ],
@@ -551,21 +545,6 @@ export function init($plugin, store) {
exact: false exact: false
}); });
configureType(HCI.VPC, { hiddenNamespaceGroupButton: true, canYaml: false });
virtualType({
labelKey: 'harvester.vpc.label',
name: HCI.VPC,
namespaced: true,
weight: 187,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VPC }
},
exact: false,
ifHaveType: HCI.VPC,
});
configureType(HCI.SNAPSHOT, { configureType(HCI.SNAPSHOT, {
isCreatable: false, isCreatable: false,
location: { location: {
-15
View File
@@ -1,6 +1,3 @@
import { HCI } from '../types';
// TODO: delete this not used variable
export const MemoryUnit = [{ export const MemoryUnit = [{
label: 'Mi', label: 'Mi',
value: 'Mi' value: 'Mi'
@@ -72,7 +69,6 @@ export const ADD_ONS = {
RANCHER_MONITORING: 'rancher-monitoring', RANCHER_MONITORING: 'rancher-monitoring',
VM_IMPORT_CONTROLLER: 'vm-import-controller', VM_IMPORT_CONTROLLER: 'vm-import-controller',
LVM_DRIVER: 'lvm.driver.harvesterhci.io', LVM_DRIVER: 'lvm.driver.harvesterhci.io',
KUBEOVN_OPERATOR: 'kubeovn-operator',
}; };
export const CSI_SECRETS = { export const CSI_SECRETS = {
@@ -83,14 +79,3 @@ export const CSI_SECRETS = {
CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name', CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name',
CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace', CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace',
}; };
// 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
};
@@ -15,7 +15,6 @@ export const HCI = {
TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName', TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName',
CREATOR: 'harvesterhci.io/creator', CREATOR: 'harvesterhci.io/creator',
OS: 'harvesterhci.io/os', OS: 'harvesterhci.io/os',
GOLDEN_IMAGE: 'harvesterhci.io/goldenImage',
NETWORK_TYPE: 'network.harvesterhci.io/type', NETWORK_TYPE: 'network.harvesterhci.io/type',
VM_NAME: 'harvesterhci.io/vmName', VM_NAME: 'harvesterhci.io/vmName',
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix', VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
@@ -35,7 +34,6 @@ export const HCI = {
NODE_SCHEDULABLE: 'kubevirt.io/schedulable', NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
NETWORK_ROUTE: 'network.harvesterhci.io/route', NETWORK_ROUTE: 'network.harvesterhci.io/route',
MATCHED_NODES: 'network.harvesterhci.io/matched-nodes', MATCHED_NODES: 'network.harvesterhci.io/matched-nodes',
UPGRADE: 'harvesterhci.io/upgrade',
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image', OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade', LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
UPGRADE_STATE: 'harvesterhci.io/upgradeState', UPGRADE_STATE: 'harvesterhci.io/upgradeState',
@@ -45,13 +43,11 @@ export const HCI = {
IMAGE_SUFFIX: 'harvesterhci.io/image-type', IMAGE_SUFFIX: 'harvesterhci.io/image-type',
OS_TYPE: 'harvesterhci.io/os-type', OS_TYPE: 'harvesterhci.io/os-type',
STORAGE_PROVISIONER: 'harvesterhci.io/storageProvisioner', STORAGE_PROVISIONER: 'harvesterhci.io/storageProvisioner',
SKIP_SINGLE_REPLICA_DETACHED_VOL: 'harvesterhci.io/skipSingleReplicaDetachedVol',
HOST_REQUEST: 'management.cattle.io/pod-requests', HOST_REQUEST: 'management.cattle.io/pod-requests',
STORAGE_CLASS: 'harvesterhci.io/storageClassName', STORAGE_CLASS: 'harvesterhci.io/storageClassName',
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io', STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental', ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error', VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm', KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx', KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx',
NODE_MANUFACTURER: 'manufacturer', NODE_MANUFACTURER: 'manufacturer',
@@ -67,13 +63,4 @@ export const HCI = {
VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails', VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails',
SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId', SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId',
DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine', DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine',
K8S_ARCH: 'kubernetes.io/arch',
IMAGE_DISPLAY_NAME: 'harvesterhci.io/imageDisplayName',
CUSTOM_IP: 'harvesterhci.io/custom-ip',
IMPORTED_IMAGE: 'migration.harvesterhci.io/imported',
VM_CPU_MEMORY_HOTPLUG: 'harvesterhci.io/enableCPUAndMemoryHotplug',
FILESYSTEM_OVERHEAD: 'cdi.harvesterhci.io/filesystemOverhead',
CLONE_STRATEGY: 'cdi.harvesterhci.io/storageProfileCloneStrategy',
VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes',
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
}; };
-1
View File
@@ -1,2 +1 @@
export const CLUSTER_NETWORK = 'clusterNetwork'; export const CLUSTER_NETWORK = 'clusterNetwork';
export const VPC = 'vpc';
+5 -28
View File
@@ -28,17 +28,12 @@ export const HCI_SETTING = {
RELEASE_DOWNLOAD_URL: 'release-download-url', RELEASE_DOWNLOAD_URL: 'release-download-url',
CCM_CSI_VERSION: 'harvester-csi-ccm-versions', CCM_CSI_VERSION: 'harvester-csi-ccm-versions',
CSI_DRIVER_CONFIG: 'csi-driver-config', CSI_DRIVER_CONFIG: 'csi-driver-config',
CSI_ONLINE_EXPAND_VALIDATION: 'csi-online-expand-validation',
VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds', VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds',
NTP_SERVERS: 'ntp-servers', NTP_SERVERS: 'ntp-servers',
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs', AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes', KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled', LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio', ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
UPGRADE_CONFIG: 'upgrade-config',
VM_MIGRATION_NETWORK: 'vm-migration-network',
RANCHER_CLUSTER: 'rancher-cluster',
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio'
}; };
export const HCI_ALLOWED_SETTINGS = { export const HCI_ALLOWED_SETTINGS = {
@@ -58,9 +53,6 @@ export const HCI_ALLOWED_SETTINGS = {
featureFlag: 'autoRotateRke2CertsSetting' featureFlag: 'autoRotateRke2CertsSetting'
}, },
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' }, [HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
[HCI_SETTING.CSI_ONLINE_EXPAND_VALIDATION]: {
kind: 'json', from: 'import', featureFlag: 'csiOnlineExpandValidation'
},
[HCI_SETTING.SERVER_VERSION]: { readOnly: true }, [HCI_SETTING.SERVER_VERSION]: { readOnly: true },
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' }, [HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' }, [HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
@@ -69,13 +61,11 @@ export const HCI_ALLOWED_SETTINGS = {
kind: 'multiline', canReset: true, from: 'import' kind: 'multiline', canReset: true, from: 'import'
}, },
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' }, [HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: { kind: 'number' }, [HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: {},
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: { kind: 'number' }, [HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: {},
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { kind: 'number', featureFlag: 'supportBundleNodeCollectionTimeoutSetting' }, [HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' }, [HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' },
[HCI_SETTING.STORAGE_NETWORK]: { [HCI_SETTING.STORAGE_NETWORK]: { kind: 'custom', from: 'import' },
kind: 'custom', from: 'import', canReset: true
},
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' }, [HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' }, [HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
[HCI_SETTING.SSL_PARAMETERS]: { [HCI_SETTING.SSL_PARAMETERS]: {
@@ -97,26 +87,13 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.NTP_SERVERS]: { [HCI_SETTING.NTP_SERVERS]: {
kind: 'json', from: 'import', canReset: true kind: 'json', from: 'import', canReset: true
}, },
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { kind: 'number', featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' }, [HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: { [HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: {
kind: 'boolean', kind: 'boolean',
experimental: true, experimental: true,
featureFlag: 'longhornV2LVMSupport' featureFlag: 'longhornV2LVMSupport'
}, },
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' }, [HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
[HCI_SETTING.UPGRADE_CONFIG]: {
kind: 'json',
from: 'import',
featureFlag: 'upgradeConfigSetting',
docPath: 'UPGRADE_CONFIG_URL'
},
[HCI_SETTING.RANCHER_CLUSTER]: {
kind: 'custom', from: 'import', canReset: true, featureFlag: 'rancherClusterSetting'
},
[HCI_SETTING.MAX_HOTPLUG_RATIO]: { kind: 'number', featureFlag: 'cpuMemoryHotplug' },
[HCI_SETTING.VM_MIGRATION_NETWORK]: {
kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration',
},
}; };
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = { export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
-43
View File
@@ -1,7 +1,6 @@
/** /**
* Harvester * Harvester
*/ */
import { DESCRIPTION } from '@shell/config/table-headers';
// image // image
export const IMAGE_DOWNLOAD_SIZE = { export const IMAGE_DOWNLOAD_SIZE = {
@@ -89,45 +88,3 @@ export const MACHINE_POOLS = {
align: 'center', align: 'center',
width: 100, width: 100,
}; };
// The STORAGE_CLASS column in VM image list page
export const IMAGE_STORAGE_CLASS = {
name: 'imageStorageClass',
labelKey: 'harvester.tableHeaders.storageClass',
sort: 'imageStorageClass',
value: 'imageStorageClass',
align: 'left',
width: 150,
};
export const HARVESTER_DESCRIPTION = {
...DESCRIPTION,
width: 150,
};
// The CIDR_BLOCK column in VPC list page
export const CIDR_BLOCK = {
name: 'cidrBlock',
labelKey: 'harvester.subnet.cidrBlock.label',
sort: 'cidrBlock',
value: 'spec.cidrBlock',
align: 'left',
};
// The Protocol column in VPC list page
export const PROTOCOL = {
name: 'protocol',
labelKey: 'harvester.subnet.protocol.label',
sort: 'protocol',
value: 'spec.protocol',
align: 'left',
};
// The Provider column in VPC list page
export const PROVIDER = {
name: 'provider',
labelKey: 'harvester.subnet.provider.label',
sort: 'provider',
value: 'spec.provider',
align: 'left',
};
-21
View File
@@ -2,24 +2,3 @@ export const BACKUP_TYPE = {
BACKUP: 'backup', BACKUP: 'backup',
SNAPSHOT: 'snapshot' SNAPSHOT: 'snapshot'
}; };
export const NETWORK_TYPE = {
L2VLAN: 'L2VlanNetwork',
UNTAGGED: 'UntaggedNetwork',
OVERLAY: 'OverlayNetwork',
};
export const VOLUME_MODE = {
BLOCK: 'Block',
FILE_SYSTEM: 'Filesystem'
};
export const NETWORK_PROTOCOL = {
IPv4: 'IPv4',
IPv6: 'IPv6',
};
export const INTERNAL_STORAGE_CLASS = {
VMSTATE_PERSISTENCE: 'vmstate-persistence',
LONGHORN_STATIC: 'longhorn-static',
};
@@ -4,7 +4,6 @@ import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { LONGHORN, METRIC } from '@shell/config/types'; import { LONGHORN, METRIC } from '@shell/config/types';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import { UNIT_SUFFIX } from '../../utils/unit';
import HarvesterCPUUsed from '../../formatters/HarvesterCPUUsed'; import HarvesterCPUUsed from '../../formatters/HarvesterCPUUsed';
import HarvesterMemoryUsed from '../../formatters/HarvesterMemoryUsed'; import HarvesterMemoryUsed from '../../formatters/HarvesterMemoryUsed';
import HarvesterStorageUsed from '../../formatters/HarvesterStorageUsed'; import HarvesterStorageUsed from '../../formatters/HarvesterStorageUsed';
@@ -122,7 +121,7 @@ export default {
memoryUnits() { memoryUnits() {
const exponent = exponentNeeded(this.memoryTotal, 1024); const exponent = exponentNeeded(this.memoryTotal, 1024);
return `${ UNITS[exponent] }${ UNIT_SUFFIX }`; return `${ UNITS[exponent] }iB`;
}, },
nodeType() { nodeType() {
@@ -47,26 +47,20 @@ export default {
STATE, STATE,
NAME, NAME,
{ {
name: 'CPU', name: 'vmCPU',
label: 'CPU', labelKey: 'tableHeaders.cpu',
sort: ['displayCPU'], search: false,
value: 'displayCPU', sort: ['spec.template.spec.domain.cpu.cores'],
align: 'center', value: 'spec.template.spec.domain.cpu.cores',
dashIfEmpty: true, width: 120
}, },
{ {
name: 'Memory', name: 'vmRAM',
value: 'displayMemory', labelKey: 'glance.memory',
search: false,
sort: ['memorySort'], sort: ['memorySort'],
align: 'center', value: 'spec.template.spec.domain.resources.limits.memory',
labelKey: 'tableHeaders.memory', width: 120
formatter: 'Si',
formatterOpts: {
opts: {
increment: 1024, addSuffix: true, maxExponent: 3, minExponent: 3, suffix: 'i',
},
needParseSi: true
},
}, },
{ {
name: 'ip', name: 'ip',
@@ -9,7 +9,7 @@ import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
import Loading from '@shell/components/Loading.vue'; import Loading from '@shell/components/Loading.vue';
import SortableTable from '@shell/components/SortableTable'; import SortableTable from '@shell/components/SortableTable';
import Banner from '@components/Banner/Banner.vue'; import Banner from '@components/Banner/Banner.vue';
import { UNIT_SUFFIX } from '../../utils/unit';
import metricPoller from '@shell/mixins/metric-poller'; import metricPoller from '@shell/mixins/metric-poller';
import { import {
METRIC, NODE, LONGHORN, POD, EVENT METRIC, NODE, LONGHORN, POD, EVENT
@@ -178,7 +178,7 @@ export default {
minExponent: 3, minExponent: 3,
maxExponent: 3, maxExponent: 3,
maxPrecision: 2, maxPrecision: 2,
suffix: UNIT_SUFFIX, suffix: 'iB',
}; };
const longhornDisks = Object.keys(diskStatus).map((key) => { const longhornDisks = Object.keys(diskStatus).map((key) => {
@@ -125,7 +125,6 @@ export default {
:resource="value" :resource="value"
:mode="mode" :mode="mode"
:apply-hooks="applyHooks" :apply-hooks="applyHooks"
@error="e=>errors=e"
> >
<Tabbed <Tabbed
v-if="spec" v-if="spec"
@@ -172,9 +171,6 @@ export default {
:cpu="cpu" :cpu="cpu"
:mode="mode" :mode="mode"
:memory="memory" :memory="memory"
:max-cpu="maxCpu"
:max-memory="maxMemory"
:enable-hot-plug="cpuMemoryHotplugEnabled"
/> />
</div> </div>
<div class="row"> <div class="row">
@@ -118,10 +118,6 @@ export default {
imageName() { imageName() {
return this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_NAME] || '-'; return this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_NAME] || '-';
}, },
sourceType() {
return this.value?.spec?.sourceType;
},
} }
}; };
</script> </script>
@@ -256,16 +252,6 @@ export default {
</div> </div>
</div> </div>
<div class="row">
<div class="col span-12">
<LabelValue
:name="t('harvester.image.source')"
:value="sourceType"
class="mb-20"
/>
</div>
</div>
<div <div
v-if="errorMessage !== '-'" v-if="errorMessage !== '-'"
class="row" class="row"
@@ -125,7 +125,6 @@ export default {
:resource="value" :resource="value"
:mode="mode" :mode="mode"
:apply-hooks="applyHooks" :apply-hooks="applyHooks"
@error="e=>errors=e"
> >
<Tabbed <Tabbed
v-if="spec" v-if="spec"
@@ -172,9 +171,6 @@ export default {
:cpu="cpu" :cpu="cpu"
:mode="mode" :mode="mode"
:memory="memory" :memory="memory"
:max-cpu="maxCpu"
:max-memory="maxMemory"
:enable-hot-plug="cpuMemoryHotplugEnabled"
/> />
<div class="row mb-10"> <div class="row mb-10">
@@ -5,7 +5,6 @@ import CreateEditView from '@shell/mixins/create-edit-view';
import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress'; import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress';
import VMConsoleBar from '../../../components/VMConsoleBar'; import VMConsoleBar from '../../../components/VMConsoleBar';
import { HCI } from '../../../types'; import { HCI } from '../../../types';
import { getVmCPUMemoryValues } from '../../../utils/cpuMemory';
const UNDEFINED = 'n/a'; const UNDEFINED = 'n/a';
@@ -92,9 +91,9 @@ export default {
}, },
flavor() { flavor() {
const { cpu, memory } = getVmCPUMemoryValues(this.value); const domain = this.value?.spec?.template?.spec?.domain;
return `${ cpu } vCPU , ${ memory } ${ this.t('harvester.virtualMachine.input.memory') }`; return `${ domain.cpu?.cores } vCPU , ${ domain.resources?.limits?.memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
}, },
kernelRelease() { kernelRelease() {
@@ -17,13 +17,6 @@ export default {
default: () => { default: () => {
return {}; return {};
} }
},
vmimResource: {
type: Object,
required: true,
default: () => {
return {};
}
} }
}, },
@@ -32,12 +25,6 @@ export default {
}, },
computed: { computed: {
liveMigrationProgressEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('liveMigrationProgress');
},
migrationPhase() {
return this.vmimResource?.status?.phase || 'N/A';
},
migrationState() { migrationState() {
return this.localResource?.status?.migrationState; return this.localResource?.status?.migrationState;
}, },
@@ -71,18 +58,6 @@ export default {
<template> <template>
<div> <div>
<div
v-if="liveMigrationProgressEnabled"
class="row mb-20"
>
<div class="col span-6">
<LabelValue
:name="t('harvester.virtualMachine.detail.details.phase')"
:value="migrationPhase"
/>
</div>
</div>
<div class="row mb-20"> <div class="row mb-20">
<div class="col span-6"> <div class="col span-6">
<LabelValue <LabelValue
@@ -27,8 +27,6 @@ import { formatSi } from '@shell/utils/units';
const VM_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-detail-1/vm-info-detail?orgId=1'; const VM_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-detail-1/vm-info-detail?orgId=1';
const VM_MIGRATION_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-migration-details-1/harvester-vm-migration-details?orgId=1';
export default { export default {
name: 'VMIDetailsPage', name: 'VMIDetailsPage',
@@ -65,7 +63,6 @@ export default {
hasResourceQuotaSchema: false, hasResourceQuotaSchema: false,
switchToCloud: false, switchToCloud: false,
VM_METRICS_DETAIL_URL, VM_METRICS_DETAIL_URL,
VM_MIGRATION_DETAIL_URL,
showVmMetrics: false, showVmMetrics: false,
}; };
}, },
@@ -81,7 +78,6 @@ export default {
events: this.$store.dispatch(`${ inStore }/findAll`, { type: EVENT }), events: this.$store.dispatch(`${ inStore }/findAll`, { type: EVENT }),
allSSHs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SSH }), allSSHs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SSH }),
vmis: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMI }), vmis: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMI }),
vmims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIM }),
restore: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.RESTORE }), restore: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.RESTORE }),
}; };
@@ -135,39 +131,6 @@ export default {
return this.$store.getters[`${ inStore }/all`](EVENT); return this.$store.getters[`${ inStore }/all`](EVENT);
}, },
vmim() {
const inStore = this.$store.getters['currentProduct'].inStore;
const vmimList = this.$store.getters[`${ inStore }/all`](HCI.VMIM) || [];
const vmiName = this.vmi?.name || '';
// filter the corresponding vmims by vmim.spec.vmiName and find the latest one by creationTimestamp
const vmim = vmimList.filter((VMIM) => VMIM?.spec?.vmiName === vmiName).sort((a, b) => {
if (a?.metadata?.creationTimestamp > b?.metadata?.creationTimestamp) {
return -1;
}
return 1;
});
return vmim.length > 0 && vmim[0] ? vmim[0] : null;
},
migrationEvents() {
const migrationVMName = this.vmim?.metadata.name || '';
if (migrationVMName === '') {
return [];
}
return this.allEvents.filter((e) => {
const { creationTimestamp } = this.value?.metadata || {};
const involvedName = e?.involvedObject?.name;
return involvedName === migrationVMName && e.firstTimestamp >= creationTimestamp;
}).sort((a, b) => a.lastTimestamp > b.lastTimestamp);
},
events() { events() {
return this.allEvents.filter((e) => { return this.allEvents.filter((e) => {
const { name, creationTimestamp } = this.value?.metadata || {}; const { name, creationTimestamp } = this.value?.metadata || {};
@@ -175,6 +138,7 @@ export default {
const pvcName = this.value.persistentVolumeClaimName || []; const pvcName = this.value.persistentVolumeClaimName || [];
const involvedName = e?.involvedObject?.name; const involvedName = e?.involvedObject?.name;
const matchPVC = pvcName.find((name) => name === involvedName); const matchPVC = pvcName.find((name) => name === involvedName);
return (involvedName === name || involvedName === podName || matchPVC) && e.firstTimestamp >= creationTimestamp; return (involvedName === name || involvedName === podName || matchPVC) && e.firstTimestamp >= creationTimestamp;
@@ -193,10 +157,6 @@ export default {
vm: this.value.name vm: this.value.name
}; };
}, },
liveMigrationProgressEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('liveMigrationProgress');
},
}, },
methods: { methods: {
@@ -386,20 +346,6 @@ export default {
<Migration <Migration
:value="value" :value="value"
:vmi-resource="vmi" :vmi-resource="vmi"
:vmim-resource="vmim"
/>
<DashboardMetrics
v-if="showVmMetrics && liveMigrationProgressEnabled"
:detail-url="VM_MIGRATION_DETAIL_URL"
graph-height="640px"
:has-summary-and-detail="false"
:vars="graphVars"
class="mb-30"
/>
<Events
v-if="liveMigrationProgressEnabled"
:resource="vmi"
:events="migrationEvents"
/> />
</Tab> </Tab>
+9 -24
View File
@@ -1,13 +1,12 @@
<script> <script>
import { exceptionToErrorsArray } from '@shell/utils/error'; import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { runStrategies as runStrategyOptions } from '../config/harvester-map';
import { Card } from '@components/Card'; import { Card } from '@components/Card';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import { Checkbox } from '@components/Form/Checkbox'; import { Checkbox } from '@components/Form/Checkbox';
import AsyncButton from '@shell/components/AsyncButton'; import AsyncButton from '@shell/components/AsyncButton';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default { export default {
name: 'CloneVMModal', name: 'CloneVMModal',
@@ -15,7 +14,7 @@ export default {
emits: ['close'], emits: ['close'],
components: { components: {
AsyncButton, Banner, Checkbox, Card, LabeledInput, LabeledSelect AsyncButton, Banner, Checkbox, Card, LabeledInput
}, },
props: { props: {
@@ -29,9 +28,7 @@ export default {
return { return {
name: '', name: '',
cloneData: true, cloneData: true,
errors: [], errors: []
runStrategy: runStrategyOptions[1],
runStrategyOptions
}; };
}, },
@@ -41,9 +38,6 @@ export default {
actionResource() { actionResource() {
return this.resources[0]; return this.resources[0];
}, },
vmCloneRunStrategyEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('vmCloneRunStrategy');
},
}, },
methods: { methods: {
@@ -64,8 +58,7 @@ export default {
// deep clone // deep clone
try { try {
const payload = this.vmCloneRunStrategyEnabled ? { targetVm: this.name, runStrategy: this.runStrategy } : { targetVm: this.name }; const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
const res = await this.actionResource.doAction('clone', payload, {}, false);
if (res._status === 200 || res._status === 204) { if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', { this.$store.dispatch('growl/success', {
@@ -113,19 +106,6 @@ export default {
:label="t('harvester.modal.cloneVM.name')" :label="t('harvester.modal.cloneVM.name')"
required required
/> />
<LabeledSelect
v-if="vmCloneRunStrategyEnabled"
v-model:value="runStrategy"
label-key="harvester.virtualMachine.runStrategy"
:options="runStrategyOptions"
:mode="mode"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template> </template>
<template <template
@@ -147,6 +127,11 @@ export default {
@click="create" @click="create"
/> />
</div> </div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</template> </template>
</Card> </Card>
</template> </template>
@@ -1,175 +0,0 @@
<script>
import { mapState } from 'vuex';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { alternateLabel } from '@shell/utils/platform';
import AsyncButton from '@shell/components/AsyncButton';
import { Banner } from '@components/Banner';
import { Card } from '@components/Card';
import { escapeHtml } from '@shell/utils/string';
/**
* @name ConfirmExecutionDialog
* @description Dialog component to confirm the related resources before executing the action.
*/
export default {
name: 'ConfirmExecutionDialog',
emits: ['close'],
components: {
AsyncButton,
Banner,
Card,
},
props: {
/**
* @property resources to be deleted.
* @type {Resource[]} Array of the resource model's instance
*/
resources: {
type: Array,
required: true
}
},
data() {
return { errors: [] };
},
computed: {
...mapState('action-menu', ['modalData']),
warningMessageKey() {
return this.modalData.warningMessageKey;
},
names() {
return this.resources.map((obj) => obj.nameDisplay).slice(0, 5);
},
resourceNames() {
return this.names.reduce((res, name, i) => {
if (i >= 5) {
return res;
}
res += `<b>${ escapeHtml(name) }</b>`;
if (i === this.names.length - 1) {
res += this.plusMore;
} else {
res += i === this.resources.length - 2 ? ' and ' : ', ';
}
return res;
}, '');
},
plusMore() {
const remaining = this.resources.length - this.names.length;
return this.t('dialog.confirmExecution.andOthers', { count: remaining }, true);
},
type() {
const types = new Set(this.resources.reduce((array, each) => {
array.push(each.type);
return array;
}, []));
if (types.size > 1) {
return this.t('generic.resource', { count: this.resources.length });
}
const schema = this.resources[0]?.schema;
if ( !schema ) {
return `resource${ this.resources.length === 1 ? '' : 's' }`;
}
return this.$store.getters['type-map/labelFor'](schema, this.resources.length);
},
protip() {
return this.t('dialog.confirmExecution.protip', { alternateLabel });
},
},
methods: {
escapeHtml,
close() {
this.errors = [];
this.$emit('close');
},
async apply(buttonDone) {
try {
for (const resource of this.resources) {
await resource.doActionGrowl(this.modalData.action, {});
}
buttonDone(true);
this.close();
} catch (e) {
this.errors = exceptionToErrorsArray(e);
buttonDone(false);
}
}
}
};
</script>
<template>
<Card :show-highlight-border="false">
<template #title>
<h4 class="text-default-text">
{{ t('dialog.confirmExecution.title') }}
</h4>
</template>
<template #body>
<div class="pl-10 pr-10">
<span
v-clean-html="t(warningMessageKey, { type, names: resourceNames }, true)"
></span>
<div class="text-info mt-20">
{{ protip }}
</div>
<Banner
v-for="(error, i) in errors"
:key="i"
:label="error"
color="error"
/>
</div>
</template>
<template #actions>
<div class="actions">
<button
class="btn role-secondary"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="apply"
class="btn bg-primary ml-10"
:disabled="applyDisabled"
@click="apply"
/>
</div>
</template>
</Card>
</template>
<style lang='scss' scoped>
.modal-container {
max-width: 400px;
}
.actions {
width: 100%;
text-align: right;
}
</style>
@@ -42,25 +42,8 @@ export default {
computed: { computed: {
...mapState('action-menu', ['modalData']), ...mapState('action-menu', ['modalData']),
title() { warningMessageKey() {
return this.modalData.title || 'dialog.promptRemove.title'; return this.modalData.warningMessageKey;
},
formattedType() {
return this.type.toLowerCase();
},
warningMessage() {
if (this.modalData.warningMessage) return this.modalData.warningMessage;
const isPlural = this.type.endsWith('s');
const thisOrThese = isPlural ? 'these' : 'this';
const defaultMessage = this.t('dialog.promptRemove.warningMessage', {
type: this.formattedType,
thisOrThese,
});
return defaultMessage;
}, },
names() { names() {
@@ -87,12 +70,6 @@ export default {
return this.resources[0].nameDisplay; return this.resources[0].nameDisplay;
}, },
needConfirmation() {
const { needConfirmation = true } = this.modalData ;
return needConfirmation === true;
},
plusMore() { plusMore() {
const remaining = this.resources.length - this.names.length; const remaining = this.resources.length - this.names.length;
@@ -120,15 +97,11 @@ export default {
}, },
deleteDisabled() { deleteDisabled() {
if (!this.needConfirmation) {
return false;
}
return this.confirmName !== this.nameToMatch; return this.confirmName !== this.nameToMatch;
}, },
protip() { protip() {
return this.t('dialog.promptRemove.protip', { alternateLabel }); return this.t('promptRemove.protip', { alternateLabel });
}, },
}, },
@@ -164,26 +137,19 @@ export default {
> >
<template #title> <template #title>
<h4 class="text-default-text"> <h4 class="text-default-text">
{{ t(title, { type }, true) }} {{ t('promptRemove.title') }}
</h4> </h4>
</template> </template>
<template #body> <template #body>
<div class="pl-10 pr-10"> <div class="pl-10 pr-10">
<span <span
v-clean-html="warningMessage" v-clean-html="t(warningMessageKey, { type, names: resourceNames }, true)"
></span> ></span>
<div
v-if="needConfirmation"
class="mt-20"
>
<div class="mt-10 mb-10"> <div class="mt-10 mb-10">
<span <span
v-clean-html="t('dialog.promptRemove.confirmName', { v-clean-html="t('promptRemove.confirmName', { nameToMatch: escapeHtml(nameToMatch) }, true)"
type: formattedType,
nameToMatch: escapeHtml(nameToMatch)
}, true)"
></span> ></span>
</div> </div>
<div class="mb-10"> <div class="mb-10">
@@ -197,7 +163,6 @@ export default {
<div class="text-info mt-20"> <div class="text-info mt-20">
{{ protip }} {{ protip }}
</div> </div>
</div>
<Banner <Banner
v-for="(error, i) in errors" v-for="(error, i) in errors"
:key="i" :key="i"
+149
View File
@@ -0,0 +1,149 @@
<script>
import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import { Checkbox } from '@components/Form/Checkbox';
import AsyncButton from '@shell/components/AsyncButton';
import { LabeledInput } from '@components/Form/LabeledInput';
export default {
name: 'CloneVMModal',
emits: ['close'],
components: {
AsyncButton, Banner, Checkbox, Card, LabeledInput
},
props: {
resources: {
type: Array,
required: true
}
},
data() {
return {
name: '',
cloneData: true,
errors: []
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0];
},
},
methods: {
close() {
this.name = '';
this.$emit('close');
},
async create(buttonCb) {
// shadow clone
if (!this.cloneData) {
this.resources[0].goToClone();
buttonCb(false);
this.close();
return;
}
// deep clone
try {
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', {
title: this.t('harvester.notification.title.succeed'),
message: this.t('harvester.modal.cloneVM.message.success', { name: this.name })
}, { root: true });
this.close();
buttonCb(true);
} else {
const error = [res?.data] || exceptionToErrorsArray(res);
this['errors'] = error;
buttonCb(false);
}
} catch (err) {
const error = err?.data || err;
const message = exceptionToErrorsArray(error);
this['errors'] = message;
buttonCb(false);
}
}
},
};
</script>
<template>
<Card :show-highlight-border="false">
<template #title>
{{ t('harvester.modal.cloneVM.title') }}
</template>
<template #body>
<Checkbox
v-model:value="cloneData"
class="mb-10"
label-key="harvester.modal.cloneVM.type"
/>
<LabeledInput
v-show="cloneData"
v-model:value="name"
class="mb-20"
:label="t('harvester.modal.cloneVM.name')"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
:label="err"
color="error"
/>
</template>
<template #actions>
<div class="actions">
<div class="buttons">
<button
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="create"
:action-label="cloneData ? t('harvester.modal.cloneVM.action.create') : t('harvester.modal.cloneVM.action.clone')"
:disabled="cloneData && !name"
@click="create"
/>
</div>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -57,10 +57,6 @@ export default {
if (!!pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) { if (!!pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
return false; return false;
} }
// we won't show golden image volume in the hot plug volume modal
if (pvc.isGoldenImageVolume) {
return false;
}
return !pvc.attachVM; return !pvc.attachVM;
}) })
@@ -1,185 +0,0 @@
<script>
import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex';
import { getVmCPUMemoryValues } from '../utils/cpuMemory';
import UnitInput from '@shell/components/form/UnitInput';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import { GIBIBYTE } from '../utils/unit';
export default {
name: 'CPUMemoryHotplugModal',
emits: ['close'],
components: {
AsyncButton, Card, Banner, UnitInput
},
props: {
resources: {
type: Array,
required: true
}
},
data() {
const { cpu, memory } = getVmCPUMemoryValues(this.resources[0] || {});
return {
cpu,
memory,
errors: [],
GIBIBYTE
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0] || {};
},
maxResourcesMessage() {
const { maxCpu, maxMemory } = getVmCPUMemoryValues(this.actionResource);
if (maxCpu && maxMemory) {
return this.t('harvester.modal.cpuMemoryHotplug.maxResourcesMessage', { maxCpu, maxMemory });
}
return '';
}
},
methods: {
close() {
this.cpu = '';
this.memory = '';
this.$emit('close');
},
change() {
if (parseInt(this.memory, 10) < 1 ) {
this.memory = '1Gi';
}
if (this.cpu < 1) {
this.cpu = 1;
}
},
async save(buttonCb) {
if (this.actionResource) {
try {
const res = await this.actionResource.doAction('cpuAndMemoryHotplug', { sockets: this.cpu, memory: this.memory });
if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', {
title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.modal.cpuMemoryHotplug.success', { vm: this.actionResource.nameDisplay })
}, { root: true });
this.close();
buttonCb(true);
} else {
const error = [res?.data] || exceptionToErrorsArray(res);
this['errors'] = error;
buttonCb(false);
}
} catch (err) {
const error = err?.data || err;
const message = exceptionToErrorsArray(error);
this['errors'] = message;
buttonCb(false);
}
}
},
}
};
</script>
<template>
<Card
ref="modal"
name="modal"
:show-highlight-border="false"
>
<template #title>
<h4
v-clean-html="t('harvester.modal.cpuMemoryHotplug.title')"
class="text-default-text"
/>
</template>
<template #body>
<Banner
v-if="maxResourcesMessage"
:label="maxResourcesMessage"
color="info"
/>
<UnitInput
v-model:value="cpu"
:label="t('harvester.virtualMachine.input.cpu')"
:delay="0"
required
suffix="C"
class="mb-20"
:mode="mode"
:min="1"
@update:value="change"
/>
<UnitInput
v-model:value="memory"
:label="t('harvester.virtualMachine.input.memory')"
:mode="mode"
:input-exponent="3"
:delay="0"
:min="1"
:increment="1024"
:output-modifier="true"
:disabled="disabled"
:suffix="GIBIBYTE"
class="mb-20"
@update:value="change"
/>
<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"
@click="save"
/>
</div>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -10,7 +10,6 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import { NAMESPACE, STORAGE_CLASS } from '@shell/config/types'; import { NAMESPACE, STORAGE_CLASS } from '@shell/config/types';
import { allHash } from '@shell/utils/promise'; import { allHash } from '@shell/utils/promise';
import { isInternalStorageClass } from '../utils/storage-class';
export default { export default {
name: 'HarvesterExportImageDialog', name: 'HarvesterExportImageDialog',
@@ -35,14 +34,9 @@ export default {
await allHash(hash); await allHash(hash);
const allStorages = this.allStorageClasses; const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find((s) => s.isDefault);
const defaultStorage = allStorages.find((s) => s.isDefault);
if (this.isLonghornV1Volume) {
this['storageClassName'] = defaultStorage?.metadata?.name || 'longhorn'; this['storageClassName'] = defaultStorage?.metadata?.name || 'longhorn';
} else {
this['storageClassName'] = this.nonLonghornV1StorageClasses[0]?.metadata?.name || '';
}
}, },
data() { data() {
@@ -60,7 +54,7 @@ export default {
...mapGetters({ t: 'i18n/t' }), ...mapGetters({ t: 'i18n/t' }),
actionResource() { actionResource() {
return this.resources[0] || {}; return this.resources[0];
}, },
namespaces() { namespaces() {
@@ -79,48 +73,24 @@ export default {
return out; return out;
}, },
isLonghornV1Volume() {
return this.actionResource?.isLonghornV1 === true;
},
disableSave() { disableSave() {
return !(this.name && this.namespace && this.storageClassName); return !(this.name && this.namespace && this.storageClassName);
}, },
allStorageClasses() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/all`](STORAGE_CLASS) || [];
},
nonLonghornV1StorageClasses() {
return this.allStorageClasses.filter((s) => s.isLonghornV1 === false) || [];
},
storageClassOptions() { storageClassOptions() {
let storages = this.allStorageClasses; const inStore = this.$store.getters['currentProduct'].inStore;
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
// Volume with non-longhorn v1 sc can't be exported to longhorn v1 sc image const out = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
if (!this.isLonghornV1Volume) { const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
storages = this.allStorageClasses.filter((s) => s.isLonghornV1 === false);
}
const options = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
const isInternal = isInternalStorageClass(s.metadata?.name);
if (isInternal) {
label += ` (${ this.t('harvester.storage.internal.label') })`;
}
return { return {
label, label,
value: s.name, value: s.name,
disabled: isInternal,
}; };
}) || []; }) || [];
return options; return out;
}, },
}, },
@@ -1,165 +0,0 @@
<script>
import { mapGetters } from 'vuex';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { HCI } from '../types';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import AppModal from '@shell/components/AppModal';
export default {
name: 'HarvesterImageDownloaderDialog',
emits: ['close'],
components: {
AsyncButton, Banner, Card, AppModal
},
props: {
resources: {
type: Array,
required: true
}
},
data() {
return { errors: [], isOpen: false };
},
computed: {
...mapGetters({ t: 'i18n/t' }),
downloadImageInProgress() {
return this.$store.getters['harvester-common/isDownloadImageInProgress'];
},
image() {
return this.resources[0] || {};
},
imageName() {
return this.image?.name || '';
},
imageVirtualSize() {
return this.image?.virtualSize || this.image?.downSize || '';
}
},
methods: {
async cancelDownload() {
const url = this.image?.links?.downloadcancel;
if (url) {
await this.$store.dispatch('harvester/request', { url });
}
},
async close() {
if (this.downloadImageInProgress) {
this.$store.commit('harvester-common/setDownloadImageCancel', true);
this.$store.commit('harvester-common/setDownloadImageInProgress', false);
await this.cancelDownload();
}
this.$emit('close');
},
async startDownload(buttonCb) {
// clean the download image CRD first.
await this.cancelDownload();
this.$store.commit('harvester-common/setDownloadImageCancel', false);
this.$store.commit('harvester-common/setDownloadImageInProgress', false);
this.errors = [];
const name = this.image?.name || '';
const namespace = this.image?.namespace || '';
const imageCrd = {
apiVersion: 'harvesterhci.io/v1beta1',
type: HCI.VM_IMAGE_DOWNLOADER,
kind: 'VirtualMachineImageDownloader',
metadata: {
name,
namespace
},
spec: { imageName: name }
};
const inStore = this.$store.getters['currentProduct'].inStore;
const imageCreate = await this.$store.dispatch(`${ inStore }/create`, imageCrd);
try {
await imageCreate.save();
this.$store.commit('harvester-common/setDownloadImageId', `${ namespace }/${ name }`, { root: true });
this.$store.dispatch('harvester-common/downloadImageProgress', { root: true });
} catch (err) {
this.errors = exceptionToErrorsArray(err);
buttonCb(false);
}
}
},
};
</script>
<template>
<app-modal
class="image-downloader-modal"
name="image-download-dialog"
height="auto"
:width="600"
:click-to-close="false"
@close="close"
>
<Card :show-highlight-border="false">
<template #title>
{{ t('harvester.modal.downloadImage.title') }}
</template>
<template #body>
<Banner color="info">
{{ t('harvester.modal.downloadImage.banner', { size: imageVirtualSize }) }}
</Banner>
{{ t('harvester.modal.downloadImage.startMessage') }}
<br /><br />
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
<div class="actions">
<div class="buttons">
<button
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
type="submit"
mode="download"
:disabled="downloadImageInProgress"
@click="startDownload"
/>
</div>
</div>
</template>
</Card>
</app-modal>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -5,7 +5,6 @@ import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton'; import AsyncButton from '@shell/components/AsyncButton';
import UnitInput from '@shell/components/form/UnitInput'; import UnitInput from '@shell/components/form/UnitInput';
import { exceptionToErrorsArray } from '@shell/utils/error'; import { exceptionToErrorsArray } from '@shell/utils/error';
import { GIBIBYTE } from '../utils/unit';
export default { export default {
name: 'HarvesterVMQuotaDialog', name: 'HarvesterVMQuotaDialog',
@@ -30,7 +29,6 @@ export default {
data() { data() {
return { return {
GIBIBYTE,
totalSnapshotSize: '', totalSnapshotSize: '',
errors: [] errors: []
}; };
@@ -46,6 +44,7 @@ export default {
}, },
methods: { methods: {
close() { close() {
this.totalSnapshotSize = ''; this.totalSnapshotSize = '';
this.$emit('close'); this.$emit('close');
@@ -93,7 +92,7 @@ export default {
:input-exponent="3" :input-exponent="3"
:increment="1024" :increment="1024"
:output-modifier="true" :output-modifier="true"
:suffix="GIBIBYTE" suffix="GiB"
class="mb-20" class="mb-20"
/> />
<Banner <Banner
+56 -218
View File
@@ -1,5 +1,4 @@
<script> <script>
import { NAMESPACE } from '@shell/config/types';
import { randomStr } from '@shell/utils/string'; import { randomStr } from '@shell/utils/string';
import { exceptionToErrorsArray, stringify } from '@shell/utils/error'; import { exceptionToErrorsArray, stringify } from '@shell/utils/error';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
@@ -7,15 +6,7 @@ import AsyncButton from '@shell/components/AsyncButton';
import GraphCircle from '@shell/components/graph/Circle'; import GraphCircle from '@shell/components/graph/Circle';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import AppModal from '@shell/components/AppModal'; import AppModal from '@shell/components/AppModal';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import UnitInput from '@shell/components/form/UnitInput';
import { HCI } from '../types'; import { HCI } from '../types';
import { HCI_SETTING } from '../config/settings';
import { DOC } from '../config/doc-links';
import { docLink } from '../utils/feature-flags';
const SELECT_ALL = 'select_all';
const UNSELECT_ALL = 'unselect_all';
export default { export default {
name: 'SupportBundle', name: 'SupportBundle',
@@ -26,36 +17,14 @@ export default {
AsyncButton, AsyncButton,
Banner, Banner,
AppModal, AppModal,
LabeledSelect,
UnitInput
},
async fetch() {
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
try {
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
const response = await this.$store.dispatch('harvester/request', { url });
this.defaultNamespaces = response.data || [];
} catch (error) {
this.defaultNamespaces = [];
}
}, },
data() { data() {
return { return {
isOpen: false,
errors: [],
version: '',
clusterName: '',
url: '', url: '',
description: '', description: '',
namespaces: [], errors: [],
defaultNamespaces: [], isOpen: false,
timeout: '',
expiration: '',
nodeTimeout: '',
}; };
}, },
@@ -70,51 +39,23 @@ export default {
percentage() { percentage() {
return this.$store.getters['harvester-common/getBundlePercentage']; return this.$store.getters['harvester-common/getBundlePercentage'];
}, }
availableNamespaces() {
const allNamespaces = this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
const defaultNamespacesIds = this.defaultNamespaces.map((ns) => ns.id);
return allNamespaces.filter((ns) => !defaultNamespacesIds.includes(ns) || this.namespaces.includes(ns));
},
namespaceOptions() {
if (this.availableNamespaces.length === 0) return [];
const allSelected = this.namespaces.length === this.availableNamespaces.length &&
this.availableNamespaces.every((ns) => this.namespaces.includes(ns));
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
return [controlOption, ...this.availableNamespaces];
},
docLink() {
const version = this.$store.getters['harvester-common/getServerVersion']();
return docLink(DOC.SUPPORT_BUNDLE_NAMESPACES, version);
},
customSupportBundleFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
},
}, },
watch: { watch: {
isShowBundleModal: { isShowBundleModal: {
immediate: true,
handler(show) { handler(show) {
this.isOpen = show; if (show) {
this.$nextTick(() => {
this.isOpen = true;
});
} else {
this.isOpen = false;
this.url = '';
this.description = '';
} }
}, },
immediate: true
isOpen(newVal) {
if (newVal) {
this.loadDefaultSettings();
} else {
this.resetForm();
}
}, },
}, },
@@ -124,93 +65,33 @@ export default {
close() { close() {
this.isOpen = false; this.isOpen = false;
this.$store.commit('harvester-common/toggleBundleModal', false); this.$store.commit('harvester-common/toggleBundleModal', false);
}, this.backUpName = '';
loadDefaultSettings() {
const cluster = this.$store.getters['currentCluster'];
const versionSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SERVER_VERSION);
const namespacesSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES);
const timeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT);
const expirationSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION);
const nodeTimeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT);
this.version = versionSetting?.currentVersion || '';
this.clusterName = cluster?.id || '';
this.namespaces = (namespacesSetting?.value ?? namespacesSetting?.default ?? '').split(',').map((ns) => ns.trim()).filter((ns) => ns);
this.timeout = timeoutSetting?.value ?? timeoutSetting?.default ?? '';
this.expiration = expirationSetting?.value ?? expirationSetting?.default ?? '';
this.nodeTimeout = nodeTimeoutSetting?.value ?? nodeTimeoutSetting?.default ?? '';
this.url = '';
this.description = '';
this.errors = [];
},
resetForm() {
this.url = '';
this.description = '';
this.namespaces = [];
this.timeout = '';
this.expiration = '';
this.nodeTimeout = '';
this.errors = [];
},
updateNamespaces(selected) {
if (selected.includes(SELECT_ALL)) {
this.namespaces = [...this.availableNamespaces];
} else if (selected.includes(UNSELECT_ALL)) {
this.namespaces = [];
} else {
this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL);
}
},
updateNumberValue(field, value) {
if (value === '' || value === null || isNaN(value)) {
this[field] = '';
return;
}
const num = Number(value);
const isValid = Number.isInteger(num) && num >= 0;
this[field] = isValid ? String(num) : '';
},
onKeyDown(e) {
if (['e', 'E', '+', '-', '.'].includes(e.key)) {
e.preventDefault();
}
}, },
async save(buttonCb) { async save(buttonCb) {
this.errors = []; this.errors = [];
const name = `bundle-${ this.clusterName }-${ this.version }-${ randomStr(5).toLowerCase() }`; const name = `bundle-${ randomStr(5).toLowerCase() }`;
const namespace = 'harvester-system'; const namespace = 'harvester-system';
const spec = {
description: this.description.trim(),
...(this.url.trim() && { issueURL: this.url.trim() }),
...(this.namespaces.length > 0 && { extraCollectionNamespaces: this.namespaces }),
...(this.timeout !== '' && { timeout: Number(this.timeout) }),
...(this.expiration !== '' && { expiration: Number(this.expiration) }),
...(this.nodeTimeout !== '' && { nodeTimeout: Number(this.nodeTimeout) }),
};
const bundleCrd = { const bundleCrd = {
apiVersion: 'harvesterhci.io/v1beta1', apiVersion: 'harvesterhci.io/v1beta1',
type: HCI.SUPPORT_BUNDLE, type: HCI.SUPPORT_BUNDLE,
kind: 'SupportBundle', kind: 'SupportBundle',
metadata: { name, namespace }, metadata: {
spec, name,
namespace
},
spec: {
issueURL: this.url,
description: this.description
}
}; };
try {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd); const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
try {
await bundleValue.save(); await bundleValue.save();
this.$store.commit('harvester-common/setLatestBundleId', `${ namespace }/${ name }`, { root: true }); this.$store.commit('harvester-common/setLatestBundleId', `${ namespace }/${ name }`, { root: true });
@@ -237,86 +118,44 @@ export default {
@close="close" @close="close"
> >
<div class="p-20"> <div class="p-20">
<h2>{{ t('harvester.modal.bundle.title') }}</h2> <h2>
<div class="content"> {{ t('harvester.modal.bundle.title') }}
</h2>
<div <div
v-if="bundlePending" v-if="!bundlePending"
class="circle mb-20" class="content"
> >
<LabeledInput
v-model:value="url"
:label="t('harvester.modal.bundle.url')"
class="mb-20"
/>
<LabeledInput
v-model:value="description"
:label="t('harvester.modal.bundle.description')"
type="multiline"
:min-height="120"
required
/>
</div>
<div
v-else
class="content"
>
<div class="circle">
<GraphCircle <GraphCircle
primary-stroke-color="green" primary-stroke-color="green"
secondary-stroke-color="lightgrey" secondary-stroke-color="white"
:stroke-width="6" :stroke-width="6"
:percentage="percentage" :percentage="percentage"
:show-text="true" :show-text="true"
/> />
</div> </div>
<template v-else> </div>
<p
v-if="customSupportBundleFeatureEnabled"
v-clean-html="t('harvester.modal.bundle.tip', { doc: docLink }, true)"
class="mb-20"
/>
<LabeledInput
v-model:value="url"
:label="t('harvester.modal.bundle.url')"
class="mb-10"
/>
<LabeledInput
v-model:value="description"
required
:label="t('harvester.modal.bundle.description')"
type="multiline"
:min-height="80"
class="mb-10"
/>
<template v-if="customSupportBundleFeatureEnabled">
<LabeledSelect
v-model:value="namespaces"
:label="t('harvester.modal.bundle.namespaces.label')"
:clearable="true"
:multiple="true"
:options="namespaceOptions"
class="mb-10 label-select"
:tooltip="t('harvester.modal.bundle.namespaces.tooltip', _, true)"
@update:value="updateNamespaces"
/>
<UnitInput
v-model:value="timeout"
:label="t('harvester.modal.bundle.timeout.label')"
class="mb-10"
type="number"
:min="0"
:tooltip="t('harvester.modal.bundle.timeout.tooltip', _, true)"
:suffix="timeout > 1 ? 'Minutes' : 'Minute'"
@keydown="onKeyDown"
@update:value="val => updateNumberValue('timeout', val)"
/>
<UnitInput
v-model:value="expiration"
:label="t('harvester.modal.bundle.expiration.label')"
class="mb-10"
type="number"
:min="0"
:tooltip="t('harvester.modal.bundle.expiration.tooltip', _, true)"
:suffix="expiration > 1 ? 'Minutes' : 'Minute'"
@keydown="onKeyDown"
@update:value="val => updateNumberValue('expiration', val)"
/>
<UnitInput
v-model:value="nodeTimeout"
:label="t('harvester.modal.bundle.nodeTimeout.label')"
class="mb-10"
type="number"
:min="0"
:tooltip="t('harvester.modal.bundle.nodeTimeout.tooltip', _, true)"
:suffix="nodeTimeout > 1 ? 'Minutes' : 'Minute'"
@keydown="onKeyDown"
@update:value="val => updateNumberValue('nodeTimeout', val)"
/>
</template>
</template>
<div <div
v-for="(err, idx) in errors" v-for="(err, idx) in errors"
:key="idx" :key="idx"
@@ -326,6 +165,7 @@ export default {
:label="stringify(err)" :label="stringify(err)"
/> />
</div> </div>
<div class="footer mt-20"> <div class="footer mt-20">
<button <button
class="btn btn-sm role-secondary mr-10" class="btn btn-sm role-secondary mr-10"
@@ -333,6 +173,7 @@ export default {
> >
{{ t('generic.close') }} {{ t('generic.close') }}
</button> </button>
<AsyncButton <AsyncButton
type="submit" type="submit"
mode="generate" mode="generate"
@@ -342,7 +183,6 @@ export default {
/> />
</div> </div>
</div> </div>
</div>
</app-modal> </app-modal>
</div> </div>
</template> </template>
@@ -354,10 +194,6 @@ export default {
max-height: 100vh; max-height: 100vh;
} }
.labeled-select.taggable ::v-deep(.vs__selected-options .vs__selected.vs__selected > button) {
margin: 0 7px;
}
.bundle { .bundle {
cursor: pointer; cursor: pointer;
color: var(--primary); color: var(--primary);
@@ -368,8 +204,10 @@ export default {
} }
.content { .content {
height: 218px;
.circle { .circle {
padding: 10px 0; padding-top: 20px;
height: 160px; height: 160px;
} }
} }
@@ -73,10 +73,8 @@ export default {
data.spec.clusterNetwork = this.clusterNetwork; data.spec.clusterNetwork = this.clusterNetwork;
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl'](`v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`);
await this.$store.dispatch('harvester/request', { await this.$store.dispatch('harvester/request', {
url, url: `/v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`,
method: 'PUT', method: 'PUT',
data, data,
}); });
@@ -38,7 +38,6 @@ export default {
:mode="mode" :mode="mode"
:errors="errors" :errors="errors"
@finish="save" @finish="save"
@error="e=>errors=e"
> >
<component <component
:is="currentComponent" :is="currentComponent"
@@ -80,7 +80,6 @@ export default {
:apply-hooks="applyHooks" :apply-hooks="applyHooks"
@finish="save" @finish="save"
@cancel="done" @cancel="done"
@error="e=>errors=e"
> >
<NameNsDescription <NameNsDescription
:value="value" :value="value"
@@ -20,7 +20,7 @@ import Loading from '@shell/components/Loading.vue';
import MessageLink from '@shell/components/MessageLink'; import MessageLink from '@shell/components/MessageLink';
import { ADD_ONS } from '@pkg/harvester/config/harvester-map'; import { ADD_ONS } from '@pkg/harvester/config/harvester-map';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester'; import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester';
import { UNIT_SUFFIX } from '../../utils/unit';
import { _EDIT } from '@shell/config/query-params'; import { _EDIT } from '@shell/config/query-params';
import { sortBy } from '@shell/utils/sort'; import { sortBy } from '@shell/utils/sort';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
@@ -185,7 +185,7 @@ export default {
minExponent: 3, minExponent: 3,
maxExponent: 3, maxExponent: 3,
maxPrecision: 2, maxPrecision: 2,
suffix: UNIT_SUFFIX, suffix: 'iB',
}; };
const longhornDisks = Object.keys(diskStatus).map((key) => { const longhornDisks = Object.keys(diskStatus).map((key) => {
@@ -457,7 +457,7 @@ export default {
const devPath = d.spec?.devPath; const devPath = d.spec?.devPath;
const deviceType = d.status?.deviceStatus?.details?.deviceType; const deviceType = d.status?.deviceStatus?.details?.deviceType;
const sizeBytes = d.status?.deviceStatus?.capacity?.sizeBytes; const sizeBytes = d.status?.deviceStatus?.capacity?.sizeBytes;
const size = formatSi(sizeBytes, { increment: 1024, suffix: UNIT_SUFFIX }); const size = formatSi(sizeBytes, { increment: 1024 });
const parentDevice = d.status?.deviceStatus?.parentDevice; const parentDevice = d.status?.deviceStatus?.parentDevice;
const isChildAdded = this.newDisks.find((newDisk) => newDisk.blockDevice?.status?.deviceStatus?.parentDevice === devPath); const isChildAdded = this.newDisks.find((newDisk) => newDisk.blockDevice?.status?.deviceStatus?.parentDevice === devPath);
const name = d.displayName; const name = d.displayName;
@@ -75,7 +75,6 @@ export default {
:errors="errors" :errors="errors"
:apply-hooks="applyHooks" :apply-hooks="applyHooks"
@finish="save" @finish="save"
@error="e=>errors=e"
> >
<div class="header mb-20"> <div class="header mb-20">
<FileSelector <FileSelector
@@ -6,13 +6,11 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import { RadioGroup } from '@components/Form/Radio'; import { RadioGroup } from '@components/Form/Radio';
import NameNsDescription from '@shell/components/form/NameNsDescription'; import NameNsDescription from '@shell/components/form/NameNsDescription';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise'; import { allHash } from '@shell/utils/promise';
import { HCI } from '../types'; import { HCI } from '../types';
import { NETWORK_TYPE } from '../config/types';
const { L2VLAN, UNTAGGED, OVERLAY } = NETWORK_TYPE;
const AUTO = 'auto'; const AUTO = 'auto';
const MANUAL = 'manual'; const MANUAL = 'manual';
@@ -43,11 +41,14 @@ export default {
data() { data() {
const config = JSON.parse(this.value.spec.config); const config = JSON.parse(this.value.spec.config);
const annotations = this.value?.metadata?.annotations || {}; const annotations = this.value?.metadata?.annotations || {};
const layer3Network = JSON.parse(annotations[HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE] || '{}'); const layer3Network = JSON.parse(annotations[HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE] || '{}');
const type = this.value.vlanType || L2VLAN ; if ((config.bridge || '').endsWith('-br')) {
config.bridge = config.bridge.slice(0, -3);
}
const type = this.value.vlanType || 'L2VlanNetwork' ;
return { return {
config, config,
@@ -74,30 +75,6 @@ export default {
}, },
computed: { computed: {
clusterBridge: {
get() {
if (!this.config.bridge) {
return '';
}
// remove -br suffix if exists
return this.config?.bridge?.endsWith('-br') ? this.config.bridge.slice(0, -3) : '';
},
set(neu) {
if (neu === '') {
this.config.bridge = '';
return;
}
if (!neu.endsWith('-br')) {
this.config.bridge = `${ neu }-br`;
} else {
this.config.bridge = neu;
}
}
},
modeOptions() { modeOptions() {
return [{ return [{
label: this.t('harvester.network.layer3Network.mode.auto'), label: this.t('harvester.network.layer3Network.mode.auto'),
@@ -108,14 +85,6 @@ export default {
}]; }];
}, },
kubeovnVpcSubnetSupport() {
return this.$store.getters['harvester-common/getFeatureEnabled']('kubeovnVpcSubnet');
},
longhornV2LVMSupport() {
return this.$store.getters['harvester-common/getFeatureEnabled']('longhornV2LVMSupport');
},
clusterNetworkOptions() { clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || []; const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
@@ -131,55 +100,16 @@ export default {
}); });
}, },
networkTypes() { networkType() {
const types = [L2VLAN, UNTAGGED]; return ['L2VlanNetwork', 'UntaggedNetwork'];
if (this.kubeovnVpcSubnetSupport) {
types.push(OVERLAY);
}
return types;
},
isL2VlanNetwork() {
if (this.isView) {
return this.value.vlanType === L2VLAN;
}
return this.type === L2VLAN;
},
isOverlayNetwork() {
if (this.isView) {
return this.value.vlanType === OVERLAY;
}
return this.type === OVERLAY;
}, },
isUntaggedNetwork() { isUntaggedNetwork() {
if (this.isView) { if (this.isView) {
return this.value.vlanType === UNTAGGED; return this.value.vlanType === 'UntaggedNetwork';
} }
return this.type === UNTAGGED; return this.type === 'UntaggedNetwork';
}
},
watch: {
type(newType) {
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 {
this.config.type = 'bridge';
this.config.promiscMode = true;
this.config.ipam = {};
this.config.bridge = '';
delete this.config.provider;
delete this.config.server_socket;
}
} }
}, },
@@ -187,7 +117,6 @@ export default {
async saveNetwork(buttonCb) { async saveNetwork(buttonCb) {
const errors = []; const errors = [];
if (this.isL2VlanNetwork || this.isUntaggedNetwork) {
if (!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') })); errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
} }
@@ -211,8 +140,8 @@ export default {
return false; return false;
} }
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network)); this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
}
await this.save(buttonCb); await this.save(buttonCb);
}, },
@@ -237,19 +166,14 @@ export default {
updateBeforeSave() { updateBeforeSave() {
this.config.name = this.value.metadata.name; this.config.name = this.value.metadata.name;
if (this.isOverlayNetwork) {
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
delete this.config.bridge;
delete this.config.promiscMode;
delete this.config.vlan;
delete this.config.ipam;
}
if (this.isUntaggedNetwork) { if (this.isUntaggedNetwork) {
delete this.config.vlan; delete this.config.vlan;
} }
this.value.spec.config = JSON.stringify({ ...this.config }); this.value.spec.config = JSON.stringify({
...this.config,
bridge: `${ this.config.bridge }-br`,
});
}, },
} }
}; };
@@ -263,7 +187,6 @@ export default {
:errors="errors" :errors="errors"
:apply-hooks="applyHooks" :apply-hooks="applyHooks"
@finish="saveNetwork" @finish="saveNetwork"
@error="e=>errors=e"
> >
<NameNsDescription <NameNsDescription
ref="nd" ref="nd"
@@ -285,15 +208,14 @@ export default {
<LabeledSelect <LabeledSelect
v-model:value="type" v-model:value="type"
class="mb-20" class="mb-20"
:options="networkTypes" :options="networkType"
:mode="mode" :mode="mode"
:disabled="isEdit"
:label="t('harvester.fields.type')" :label="t('harvester.fields.type')"
required required
/> />
<LabeledInput <LabeledInput
v-if="isL2VlanNetwork" v-if="!isUntaggedNetwork"
v-model:value.number="config.vlan" v-model:value.number="config.vlan"
class="mb-20" class="mb-20"
required required
@@ -303,20 +225,25 @@ export default {
:mode="mode" :mode="mode"
@update:value="input" @update:value="input"
/> />
<div class="row">
<div
class="col span-12"
>
<LabeledSelect <LabeledSelect
v-if="!isOverlayNetwork" v-model:value="config.bridge"
v-model:value="clusterBridge"
class="mb-20" class="mb-20"
:label="t('harvester.network.clusterNetwork.label')" :label="t('harvester.network.clusterNetwork.label')"
required required
:disabled="isEdit"
:options="clusterNetworkOptions" :options="clusterNetworkOptions"
:mode="mode" :mode="mode"
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')" :placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
/> />
</div>
</div>
</Tab> </Tab>
<Tab <Tab
v-if="isL2VlanNetwork" v-if="!isUntaggedNetwork"
name="layer3Network" name="layer3Network"
:label="t('harvester.network.tabs.layer3Network')" :label="t('harvester.network.tabs.layer3Network')"
:weight="98" :weight="98"
@@ -84,10 +84,6 @@ export default {
this.value['_type'] = TYPES.OPAQUE; this.value['_type'] = TYPES.OPAQUE;
} }
if ( this.realMode === _CLONE) {
this.value['_type'] = this.liveValue._type;
}
return { return {
isCloud, isCloud,
nodeDrivers: null, nodeDrivers: null,
+4 -39
View File
@@ -4,10 +4,8 @@ import { RadioGroup } from '@components/Form/Radio';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import { TextAreaAutoGrow } from '@components/Form/TextArea'; import { TextAreaAutoGrow } from '@components/Form/TextArea';
import UnitInput from '@shell/components/form/UnitInput';
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import { DOC } from '../config/doc-links';
import { docLink } from '../utils/feature-flags';
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings'; import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
@@ -17,8 +15,7 @@ export default {
LabeledInput, LabeledInput,
LabeledSelect, LabeledSelect,
RadioGroup, RadioGroup,
TextAreaAutoGrow, TextAreaAutoGrow
UnitInput
}, },
mixins: [CreateEditView], mixins: [CreateEditView],
@@ -60,13 +57,7 @@ export default {
return { return {
setting, setting,
description: isHarvester ? t( description: isHarvester ? t(`advancedSettings.descriptions.harv-${ this.value.id }`) : t(`advancedSettings.descriptions.${ this.value.id }`),
`advancedSettings.descriptions.harv-${ this.value.id }`,
this.getDocLinkParams()
) : t(
`advancedSettings.descriptions.${ this.value.id }`,
this.getDocLinkParams()
),
editHelp: t(`advancedSettings.editHelp.${ this.value.id }`), editHelp: t(`advancedSettings.editHelp.${ this.value.id }`),
enumOptions, enumOptions,
canReset, canReset,
@@ -137,13 +128,6 @@ export default {
await this.clusterRegistrationUrlTip(); await this.clusterRegistrationUrlTip();
} }
// remove any leading zeros (e.g. '0123' becomes 123)
if (this.setting.kind === 'number' && this.value.value) {
const num = Number(this.value.value);
this.value.value = isNaN(num) ? 0 : `${ num }`;
}
this.save(done); this.save(done);
}, },
@@ -179,18 +163,6 @@ export default {
if (typeof this.$refs.settingComp?.useDefault === 'function') { if (typeof this.$refs.settingComp?.useDefault === 'function') {
this.$refs.settingComp.useDefault(); this.$refs.settingComp.useDefault();
} }
},
getDocLinkParams() {
const setting = HCI_ALLOWED_SETTINGS[this.value.id] || HCI_SINGLE_CLUSTER_ALLOWED_SETTING[this.value.id];
if (setting?.docPath) {
const version = this.$store.getters['harvester-common/getServerVersion']();
const url = docLink(DOC[setting.docPath], version);
return { url };
}
return {};
} }
} }
}; };
@@ -205,7 +177,7 @@ export default {
:subtypes="[]" :subtypes="[]"
:can-yaml="false" :can-yaml="false"
:cancel-event="true" :cancel-event="true"
@error="e=>errors=e" @error="e => (errors = e)"
@finish="saveSettings" @finish="saveSettings"
@cancel="done" @cancel="done"
> >
@@ -266,13 +238,6 @@ export default {
:min-height="254" :min-height="254"
/> />
</div> </div>
<div v-else-if="setting.kind === 'number'">
<LabeledInput
v-model:value="value.value"
:label="t('advancedSettings.edit.value')"
type="number"
/>
</div>
<div v-else> <div v-else>
<LabeledInput <LabeledInput
v-model:value="value.value" v-model:value="value.value"
@@ -1,319 +0,0 @@
<script>
import { VOLUME_MODE } from '@pkg/harvester/config/types';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import ArrayList from '@shell/components/form/ArrayList';
import { Checkbox } from '@components/Form/Checkbox';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { VOLUME_SNAPSHOT_CLASS, HCI } from '../../types';
import { HCI_SETTING } from '../../config/settings';
import { allHash } from '@shell/utils/promise';
import { _EDIT, _CREATE } from '@shell/config/query-params';
export default {
name: 'CDISettings',
props: {
value: {
type: Object,
required: true
},
mode: {
type: String,
required: true
},
provisioner: {
type: String,
required: true
},
},
components: {
ArrayList,
Checkbox,
LabeledInput,
LabeledSelect,
},
emits: ['update:cdiSettings'],
async fetch() {
const hash = {
volumeSnapshotClasses: this.$store.dispatch(`${ this.inStore }/findAll`, { type: VOLUME_SNAPSHOT_CLASS }),
csiDriverConfigSetting: this.$store.dispatch(`${ this.inStore }/find`, { type: HCI.SETTING, id: HCI_SETTING.CSI_DRIVER_CONFIG }),
};
await allHash(hash);
if (this.mode === _CREATE ) {
this.setDefaultVolumeSnapshotClass();
} else {
this.initCDISettingsFromAnnotations();
}
},
data() {
return {
cdiSettings: {
volumeModeAccessModes: [],
volumeSnapshotClass: null,
cloneStrategy: null,
filesystemOverhead: null,
},
defaultAddValue: { volumeMode: null, accessModes: [] },
noneOption: { label: 'None', value: '' },
};
},
computed: {
inStore() {
return this.$store.getters['currentProduct'].inStore;
},
allVolumeModeOptions() {
return Object.values(VOLUME_MODE).map((mode) => ({ label: mode, value: mode }));
},
selectedVolumeModes() {
return this.cdiSettings.volumeModeAccessModes
.map((item) => item.volumeMode)
.filter(Boolean);
},
volumeModeOptions() {
return [this.noneOption, ...this.allVolumeModeOptions].filter( (option) => !this.selectedVolumeModes.includes(option.value) );
},
allVolumeModesSelected() {
return this.selectedVolumeModes.length === this.allVolumeModeOptions.length;
},
accessModeOptions() {
return [
{ label: 'ReadWriteOnce', value: 'ReadWriteOnce' },
{ label: 'ReadOnlyMany', value: 'ReadOnlyMany' },
{ label: 'ReadWriteMany', value: 'ReadWriteMany' },
{ label: 'ReadWriteOncePod', value: 'ReadWriteOncePod' },
];
},
cloneStrategyOptions() {
return [
this.noneOption,
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.copy'), value: 'copy' },
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.snapshot'), value: 'snapshot' },
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.csiClone'), value: 'csi-clone' },
];
},
volumeSnapshotClassOptions() {
const allClasses = this.$store.getters[`${ this.inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
const filtered = allClasses.filter((c) => c.driver === this.provisioner);
return [this.noneOption, ...filtered.map((c) => ({ label: c.name, value: c.name }))];
},
isFilesystemOverheadValid() {
const val = this.cdiSettings.filesystemOverhead;
if (val === null || val === '') return true;
const regex = /^(0(\.\d{1,3})?|1(\.0{1,3})?)$/;
return regex.test(val);
},
isCustomClass() {
return this.mode === _CREATE || this.mode === _EDIT;
}
},
watch: {
provisioner() {
this.resetCdiSettings();
this.$nextTick(this.setDefaultVolumeSnapshotClass);
},
cdiSettings: {
handler(val) {
this.$emit('update:cdiSettings', val);
},
deep: true,
immediate: true,
}
},
methods: {
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 defaultClass = config?.[this.provisioner]?.volumeSnapshotClassName || null;
const allClasses = this.$store.getters[`${ this.inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
const matched = allClasses.find((cls) => cls.name === defaultClass && cls.driver === this.provisioner);
this.cdiSettings.volumeSnapshotClass = matched?.name || null;
} catch (e) {
console.error('Failed to parse CSI config:', e); // eslint-disable-line no-console
this.cdiSettings.volumeSnapshotClass = null;
}
},
initCDISettingsFromAnnotations() {
const annotations = this.value?.metadata?.annotations || {};
let volumeModeAccessModes = [];
const rawVolumeMode = annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES];
if (rawVolumeMode) {
try {
const parsed = JSON.parse(rawVolumeMode);
volumeModeAccessModes = Object.entries(parsed).map(([volumeMode, accessModes]) => ({
volumeMode,
accessModes: Array.isArray(accessModes) ? accessModes : [],
}));
} catch (e) {
console.error('Failed to parse annotation:', e); // eslint-disable-line no-console
}
}
if (volumeModeAccessModes.length) {
this.cdiSettings.volumeModeAccessModes = volumeModeAccessModes;
}
if (annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS]) {
this.cdiSettings.volumeSnapshotClass = annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS];
}
if (annotations[HCI_ANNOTATIONS.CLONE_STRATEGY]) {
this.cdiSettings.cloneStrategy = annotations[HCI_ANNOTATIONS.CLONE_STRATEGY];
}
if (annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD]) {
this.cdiSettings.filesystemOverhead = annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD];
}
},
resetCdiSettings() {
this.cdiSettings.volumeModeAccessModes = [this.defaultAddValue];
this.cdiSettings.volumeSnapshotClass = null;
this.cdiSettings.cloneStrategy = null;
this.cdiSettings.filesystemOverhead = null;
},
validateFilesystemOverhead(e) {
const val = e.target.value;
this.cdiSettings.filesystemOverhead = val;
}
}
};
</script>
<template>
<ArrayList
v-model:value="cdiSettings.volumeModeAccessModes"
:initial-empty-row="true"
:show-header="true"
:mode="mode"
:title="t('harvester.storage.cdiSettings.volumeModeAccessModes.label')"
:add-label="t('harvester.storage.cdiSettings.volumeModeAccessModes.add')"
:default-add-value="defaultAddValue"
:protip="t('harvester.storage.cdiSettings.volumeModeAccessModes.tooltip')"
:add-disabled="allVolumeModesSelected"
>
<template #column-headers>
<div class="column-headers">
<div
class="row"
:class="{ custom: isCustomClass }"
>
<label
class="col span-3 value text-label mb-10"
for="volumeMode"
>
{{ t('harvester.storage.cdiSettings.volumeModeAccessModes.volumeMode') }}
</label>
<label
class="col span-9 value text-label mb-10"
for="accessModes"
>
{{ t('harvester.storage.cdiSettings.volumeModeAccessModes.accessModes.label') }}
</label>
</div>
</div>
</template>
<template #columns="{ row }">
<div class="row">
<div class="col span-3">
<LabeledSelect
id="volumeMode"
v-model:value="row.value.volumeMode"
:mode="mode"
:options="volumeModeOptions"
/>
</div>
<div
id="accessModes"
class="col span-9"
>
<Checkbox
v-for="opt in accessModeOptions"
:key="opt.value"
v-model:value="row.value.accessModes"
:value-when-true="opt.value"
:label="opt.label"
type="checkbox"
:mode="mode"
/>
</div>
</div>
</template>
</ArrayList>
<LabeledSelect
v-model:value="cdiSettings.volumeSnapshotClass"
class="select mt-20 mb-20"
:label="t('harvester.storage.cdiSettings.volumeSnapshotClass.label')"
:tooltip="t('harvester.storage.cdiSettings.volumeSnapshotClass.tooltip')"
:mode="mode"
:options="volumeSnapshotClassOptions"
/>
<LabeledSelect
v-model:value="cdiSettings.cloneStrategy"
class="select mb-20"
:label="t('harvester.storage.cdiSettings.cloneStrategy.label')"
:tooltip="t('harvester.storage.cdiSettings.cloneStrategy.tooltip')"
:mode="mode"
:options="cloneStrategyOptions"
/>
<LabeledInput
v-model:value="cdiSettings.filesystemOverhead"
class="select mb-20"
:label="t('harvester.storage.cdiSettings.fileSystemOverhead.label')"
:tooltip="t('harvester.storage.cdiSettings.fileSystemOverhead.tooltip')"
:mode="mode"
type="number"
:min="0"
:max="1"
:step="0.001"
:placeholder="t('harvester.storage.cdiSettings.fileSystemOverhead.placeholder')"
:status="isFilesystemOverheadValid ? null : 'error'"
@input="validateFilesystemOverhead"
/>
</template>
<style scoped lang="scss">
.column-headers .row.custom {
max-width: calc(100% - 75px);
}
.row {
align-items: center;
}
.select {
max-width: 480px;
}
</style>
@@ -1,5 +1,4 @@
<script> <script>
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import CruResource from '@shell/components/CruResource'; import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription'; import NameNsDescription from '@shell/components/form/NameNsDescription';
@@ -7,9 +6,11 @@ import ArrayList from '@shell/components/form/ArrayList';
import Tab from '@shell/components/Tabbed/Tab'; import Tab from '@shell/components/Tabbed/Tab';
import Tabbed from '@shell/components/Tabbed'; import Tabbed from '@shell/components/Tabbed';
import { RadioGroup } from '@components/Form/Radio'; import { RadioGroup } from '@components/Form/Radio';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import Loading from '@shell/components/Loading'; import Loading from '@shell/components/Loading';
import { _CREATE, _VIEW } from '@shell/config/query-params'; import { _CREATE, _VIEW } from '@shell/config/query-params';
import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features'; import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
import { import {
@@ -21,11 +22,11 @@ import { CSI_DRIVER } from '../../types';
import Tags from '../../components/DiskTags'; import Tags from '../../components/DiskTags';
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim'; import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass'; import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
import CDISettings from './CDISettings';
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node'; export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node';
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
const VOLUME_BINDING_MODE_IMMEDIATE = 'Immediate'; const VOLUME_BINDING_MODE_IMMEDIATE = 'Immediate';
const VOLUME_BINDING_MODE_WAIT = 'WaitForFirstConsumer'; const VOLUME_BINDING_MODE_WAIT = 'WaitForFirstConsumer';
@@ -45,7 +46,6 @@ export default {
Tabbed, Tabbed,
Loading, Loading,
Tags, Tags,
CDISettings,
}, },
mixins: [CreateEditView], mixins: [CreateEditView],
@@ -118,7 +118,6 @@ export default {
key: '', key: '',
values: [], values: [],
}, },
cdiSettings: {},
}; };
}, },
@@ -129,7 +128,7 @@ export default {
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }), namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }), storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }), longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }), csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER })
}; };
if (this.value.longhornV2LVMSupport) { if (this.value.longhornV2LVMSupport) {
@@ -199,14 +198,6 @@ export default {
return v2DataEngine.value === 'true' ? DATA_ENGINE_V2 : DATA_ENGINE_V1; return v2DataEngine.value === 'true' ? DATA_ENGINE_V2 : DATA_ENGINE_V1;
}, },
isCDISettingsFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('cdiSettings');
},
shouldShowCDISettingsTab() {
return this.isCDISettingsFeatureEnabled && this.provisioner !== `${ LONGHORN_DRIVER }_${ DATA_ENGINE_V1 }`;
},
}, },
watch: { watch: {
@@ -259,10 +250,6 @@ export default {
}); });
this.formatAllowedTopoloties(); this.formatAllowedTopoloties();
if (this.shouldShowCDISettingsTab) {
this.formatCDISettings();
}
}, },
formatAllowedTopoloties() { formatAllowedTopoloties() {
@@ -284,42 +271,6 @@ export default {
if (matchLabelExpressions.length > 0) { if (matchLabelExpressions.length > 0) {
this.value.allowedTopologies = [{ matchLabelExpressions: [...matchLabelExpressions, ...lvmMatchExpression] }]; this.value.allowedTopologies = [{ matchLabelExpressions: [...matchLabelExpressions, ...lvmMatchExpression] }];
} }
},
formatCDISettings() {
const annotations = this.value.metadata.annotations || {};
const volumeModeAccessModes = {};
this.cdiSettings.volumeModeAccessModes.forEach((setting) => {
if (setting.volumeMode && Array.isArray(setting.accessModes) && setting.accessModes.length > 0) {
volumeModeAccessModes[setting.volumeMode] = setting.accessModes;
}
});
if (Object.keys(volumeModeAccessModes).length > 0) {
annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES] = JSON.stringify(volumeModeAccessModes);
} else {
delete annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES];
}
if (this.cdiSettings.volumeSnapshotClass) {
annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS] = this.cdiSettings.volumeSnapshotClass;
} else {
delete annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS];
}
if (this.cdiSettings.cloneStrategy) {
annotations[HCI_ANNOTATIONS.CLONE_STRATEGY] = this.cdiSettings.cloneStrategy;
} else {
delete annotations[HCI_ANNOTATIONS.CLONE_STRATEGY];
}
if (this.cdiSettings.filesystemOverhead) {
annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD] = this.cdiSettings.filesystemOverhead;
} else {
delete annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD];
}
this.value.metadata.annotations = annotations;
} }
} }
}; };
@@ -454,19 +405,6 @@ export default {
</template> </template>
</ArrayList> </ArrayList>
</Tab> </Tab>
<Tab
v-if="shouldShowCDISettingsTab"
name="cdiSettings"
:label="t('harvester.storage.cdiSettings.label')"
:weight="-2"
>
<CDISettings
v-model:cdi-settings="cdiSettings"
:value="value"
:mode="mode"
:provisioner="value.provisioner"
/>
</Tab>
</Tabbed> </Tabbed>
</CruResource> </CruResource>
</template> </template>
@@ -63,7 +63,7 @@ export default {
diskSelector: null, diskSelector: null,
nodeSelector: null, nodeSelector: null,
encrypted: 'false', encrypted: 'false',
migratable: this.value.thirdPartyStorageFeatureEnabled ? 'true' : 'false', migratable: 'false',
dataEngine: DATA_ENGINE_V2 dataEngine: DATA_ENGINE_V2
}; };
} }
@@ -318,7 +318,7 @@ export default {
:label="t('harvester.storage.parameters.migratable.label')" :label="t('harvester.storage.parameters.migratable.label')"
:mode="mode" :mode="mode"
:options="migratableOptions" :options="migratableOptions"
:disabled="!value.thirdPartyStorageFeatureEnabled" :disabled="true"
/> />
</div> </div>
<div class="row mt-20"> <div class="row mt-20">
@@ -17,7 +17,6 @@ import { VM_IMAGE_FILE_FORMAT } from '../validators/vm-image';
import { OS } from '../mixins/harvester-vm'; import { OS } from '../mixins/harvester-vm';
import { HCI } from '../types'; import { HCI } from '../types';
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass'; import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
import { isInternalStorageClass } from '../utils/storage-class';
const ENCRYPT = 'encrypt'; const ENCRYPT = 'encrypt';
const DECRYPT = 'decrypt'; const DECRYPT = 'decrypt';
@@ -25,7 +24,6 @@ const CLONE = 'clone';
const DOWNLOAD = 'download'; const DOWNLOAD = 'download';
const UPLOAD = 'upload'; const UPLOAD = 'upload';
const rawORqcow2 = 'raw_qcow2'; const rawORqcow2 = 'raw_qcow2';
const LONGHORN = 'longhorn';
export default { export default {
name: 'EditImage', name: 'EditImage',
@@ -63,10 +61,10 @@ export default {
storageClasses: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }), storageClasses: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
}); });
this['storageClassName'] = this.storageClassName || this.defaultStorageClassName(); const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find((s) => s.isDefault);
this.images = this.$store.getters[`${ inStore }/all`](HCI.IMAGE);
this.storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS); this['storageClassName'] = this.storageClassName || defaultStorage?.metadata?.name || 'longhorn';
this.images = this.$store.getters[`${ inStore }/all`](HCI.IMAGE);
const { securityParameters } = this.value.spec; const { securityParameters } = this.value.spec;
@@ -103,7 +101,6 @@ export default {
return { return {
selectedImage: null, selectedImage: null,
storageClasses: [],
images: [], images: [],
url: this.value.spec.url, url: this.value.spec.url,
files: [], files: [],
@@ -111,7 +108,6 @@ export default {
headers: {}, headers: {},
fileUrl: '', fileUrl: '',
file: '', file: '',
HCI_ANNOTATIONS,
}; };
}, },
@@ -148,39 +144,18 @@ export default {
]; ];
}, },
encryptedStorageClasses() {
const inStore = this.$store.getters['currentProduct'].inStore;
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
return storages.filter((s) => s.isEncrypted);
},
nonEncryptedStorageClasses() {
const inStore = this.$store.getters['currentProduct'].inStore;
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
return storages.filter((s) => !s.isEncrypted);
},
storageClassOptions() { storageClassOptions() {
const storages = this.value.spec?.securityParameters?.cryptoOperation === ENCRYPT ? this.encryptedStorageClasses : this.nonEncryptedStorageClasses; const inStore = this.$store.getters['currentProduct'].inStore;
const filteredStorages = this.value.thirdPartyStorageFeatureEnabled ? storages.filter((s) => !s.parameters?.backingImage) : storages const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
.filter((s) => !s.parameters?.backingImage && s.provisioner !== LVM_DRIVER) ;
return filteredStorages return storages
.filter((s) => !s.parameters?.backingImage && s.provisioner !== LVM_DRIVER) // Lvm storage is not supported.
.map((s) => { .map((s) => {
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name; const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
let disabled = false;
if (isInternalStorageClass(s.name)) {
label += ` (${ this.t('harvester.storage.internal.label') })`;
disabled = true;
}
return { return {
label, label,
value: s.name, value: s.name,
disabled
}; };
}) || []; }) || [];
}, },
@@ -192,9 +167,6 @@ export default {
set(nue) { set(nue) {
this.value.metadata.annotations[HCI_ANNOTATIONS.STORAGE_CLASS] = nue; this.value.metadata.annotations[HCI_ANNOTATIONS.STORAGE_CLASS] = nue;
if (this.value.thirdPartyStorageFeatureEnabled) {
this.value.spec.targetStorageClassName = nue;
}
} }
}, },
sourceImageOptions() { sourceImageOptions() {
@@ -267,20 +239,6 @@ export default {
this.$refs.file.value = null; this.$refs.file.value = null;
} }
}, },
'value.spec.securityParameters.cryptoOperation'() {
if (this.value.spec?.securityParameters?.cryptoOperation === ENCRYPT) {
this.storageClassName = this.encryptedStorageClasses[0]?.name || '';
} else { // URL / FILE / DECRYPT should use default storage class
this.storageClassName = this.defaultStorageClassName();
}
},
'storageClassName'(neu) {
const storageClass = this.storages.find((s) => s.id === neu);
if (storageClass && this.value.thirdPartyStorageFeatureEnabled) {
this.value.spec.backend = storageClass.isLonghornV1 ? 'backingimage' : 'cdi';
}
}
}, },
methods: { methods: {
@@ -427,18 +385,6 @@ export default {
return str.toLowerCase().includes(os.value.toLowerCase()) ? os.value : false; return str.toLowerCase().includes(os.value.toLowerCase()) ? os.value : false;
} }
}); });
},
defaultStorageClassName() {
const inStore = this.$store.getters['currentProduct'].inStore;
const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find((s) => s.isDefault);
if (!defaultStorage) {
return LONGHORN;
}
// if default sc is encrypted, use longhorn as default
return defaultStorage.isEncrypted ? LONGHORN : defaultStorage?.metadata?.name;
} }
}, },
}; };
@@ -604,8 +550,6 @@ export default {
:pad-left="false" :pad-left="false"
:read-allowed="false" :read-allowed="false"
:value-can-be-empty="true" :value-can-be-empty="true"
:toggle-filter="true"
:protected-keys="[HCI_ANNOTATIONS.IMAGE_DISPLAY_NAME]"
@focusKey="focusKey" @focusKey="focusKey"
@update:value="value.setLabels($event)" @update:value="value.setLabels($event)"
> >
@@ -622,7 +566,7 @@ export default {
<input <input
v-else v-else
v-model="row[valueName]" v-model="row[valueName]"
:disabled="isView || row[keyName] === HCI_ANNOTATIONS.IMAGE_DISPLAY_NAME" :disabled="isView"
:type="'text'" :type="'text'"
:placeholder="t('keyValue.valuePlaceholder')" :placeholder="t('keyValue.valuePlaceholder')"
autocorrect="off" autocorrect="off"
@@ -6,11 +6,12 @@ import { Checkbox } from '@components/Form/Checkbox';
import CruResource from '@shell/components/CruResource'; import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription'; import NameNsDescription from '@shell/components/form/NameNsDescription';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import { Banner } from '@components/Banner'; import Labels from '@shell/components/form/Labels';
import KeyValue from '@shell/components/form/KeyValue'; import KeyValue from '@shell/components/form/KeyValue';
import NodeScheduling from '@shell/components/form/NodeScheduling'; import NodeScheduling from '@shell/components/form/NodeScheduling';
import PodAffinity from '@shell/components/form/PodAffinity'; import PodAffinity from '@shell/components/form/PodAffinity';
import UnitInput from '@shell/components/form/UnitInput'; import UnitInput from '@shell/components/form/UnitInput';
import { randomStr } from '@shell/utils/string'; import { randomStr } from '@shell/utils/string';
import { _CONFIG, _EDIT, _VIEW } from '@shell/config/query-params'; import { _CONFIG, _EDIT, _VIEW } from '@shell/config/query-params';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
@@ -46,7 +47,7 @@ export default {
PodAffinity, PodAffinity,
Reserved, Reserved,
UnitInput, UnitInput,
Banner, Labels,
KeyValue, KeyValue,
}, },
@@ -189,16 +190,6 @@ export default {
} }
this.value.spec['templateId'] = `${ namespace }/${ name }`; this.value.spec['templateId'] = `${ namespace }/${ name }`;
// inherit labels and annotations so the VM gets them when created from the template
this.value.spec.vm.metadata.labels = {
...this.value.spec.vm.metadata.labels,
...this.value.metadata.labels
};
this.value.spec.vm.metadata.annotations = {
...this.value.spec.vm.metadata.annotations,
...this.value.metadata.annotations
};
const res = await this.value.save(); const res = await this.value.save();
await this.saveSecret(res); await this.saveSecret(res);
@@ -229,7 +220,6 @@ export default {
:errors="errors" :errors="errors"
:apply-hooks="applyHooks" :apply-hooks="applyHooks"
@finish="saveVMT" @finish="saveVMT"
@error="e=>errors=e"
> >
<NameNsDescription <NameNsDescription
:value="templateValue" :value="templateValue"
@@ -257,12 +247,8 @@ export default {
:label="t('harvester.vmTemplate.tabs.basics')" :label="t('harvester.vmTemplate.tabs.basics')"
> >
<CpuMemory <CpuMemory
class="mb-20"
:cpu="cpu" :cpu="cpu"
:memory="memory" :memory="memory"
:max-cpu="maxCpu"
:max-memory="maxMemory"
:enable-hot-plug="cpuMemoryHotplugEnabled"
:disabled="isConfig" :disabled="isConfig"
@updateCpuMemory="updateCpuMemory" @updateCpuMemory="updateCpuMemory"
/> />
@@ -335,34 +321,21 @@ export default {
</Tab> </Tab>
<Tab <Tab
name="labels" :name="t('generic.labels')"
:label="t('generic.labels')" :label="t('harvester.tab.instanceLabel')"
:weight="-9" :weight="-5"
> >
<Banner color="info"> <Labels
<t k="harvester.virtualMachine.labels.banner" /> :default-container-class="'labels-and-annotations-container'"
</Banner> :value="value"
:mode="mode"
:display-side-by-side="false"
:show-annotations="false"
:show-label-title="false"
>
<template #labels="{toggler}">
<KeyValue <KeyValue
key="labels" key="labels"
:value="value.labels"
:add-label="t('labels.addLabel')"
:mode="mode"
:read-allowed="false"
:value-can-be-empty="true"
@update:value="value.setLabels($event)"
/>
</Tab>
<Tab
name="instanceLabel"
:label="t('harvester.tab.instanceLabel')"
:weight="-10"
>
<Banner color="info">
<t k="harvester.virtualMachine.instanceLabels.banner" />
</Banner>
<KeyValue
key="instance-labels"
:value="value.instanceLabels" :value="value.instanceLabels"
:protected-keys="value.systemLabels || []" :protected-keys="value.systemLabels || []"
:toggle-filter="toggler" :toggle-filter="toggler"
@@ -370,29 +343,12 @@ export default {
:mode="mode" :mode="mode"
:read-allowed="false" :read-allowed="false"
:value-can-be-empty="true" :value-can-be-empty="true"
@update:value="value.setInstanceLabels($event)" @input="value.setInstanceLabels($event)"
/>
</Tab>
<Tab
name="annotations"
:label="t('harvester.tab.annotations')"
:weight="-11"
>
<Banner color="info">
<t k="harvester.virtualMachine.annotations.banner" />
</Banner>
<KeyValue
key="annotations"
:value="value.annotations"
:protected-keys="value.systemAnnotations || []"
:toggle-filter="true"
:add-label="t('labels.addAnnotation')"
:mode="mode"
:read-allowed="false"
:value-can-be-empty="true"
@update:value="value.setAnnotations($event)"
/> />
</template>
</Labels>
</Tab> </Tab>
<Tab <Tab
name="advanced" name="advanced"
:label="t('harvester.tab.advanced')" :label="t('harvester.tab.advanced')"
@@ -478,7 +434,7 @@ export default {
<div class="spacer"></div> <div class="spacer"></div>
<Checkbox <Checkbox
v-if="value.cpuPinningFeatureEnabled" v-if="value.cpuPinningFeatureEnabled"
v-model:value="cpuPinning" v-model="cpuPinning"
class="check" class="check"
type="checkbox" type="checkbox"
tooltip-key="harvester.virtualMachine.cpuPinning.tooltip" tooltip-key="harvester.virtualMachine.cpuPinning.tooltip"
@@ -509,29 +465,11 @@ export default {
:mode="mode" :mode="mode"
/> />
<Checkbox
v-if="value.tpmPersistentStateFeatureEnabled && tpmEnabled"
v-model:value="tpmPersistentStateEnabled"
class="check"
type="checkbox"
:label="t('harvester.virtualMachine.advancedOptions.tpmPersistentState')"
:mode="mode"
/>
<Checkbox <Checkbox
v-model:value="efiEnabled" v-model:value="efiEnabled"
class="check" class="check"
type="checkbox" type="checkbox"
:label="t('harvester.virtualMachine.advancedOptions.efiEnabled')" :label="t('harvester.virtualMachine.efiEnabled')"
:mode="mode"
/>
<Checkbox
v-if="value.efiPersistentStateFeatureEnabled && efiEnabled"
v-model:value="efiPersistentStateEnabled"
class="check"
type="checkbox"
:label="t('harvester.virtualMachine.advancedOptions.efiPersistentState')"
:mode="mode" :mode="mode"
/> />
@@ -540,7 +478,7 @@ export default {
v-model:value="secureBoot" v-model:value="secureBoot"
class="check" class="check"
type="checkbox" type="checkbox"
:label="t('harvester.virtualMachine.advancedOptions.secureBoot')" :label="t('harvester.virtualMachine.secureBoot')"
:mode="mode" :mode="mode"
/> />
</Tab> </Tab>
+6 -96
View File
@@ -21,9 +21,6 @@ import { InterfaceOption, VOLUME_DATA_SOURCE_KIND } from '../config/harvester-ma
import { HCI, VOLUME_SNAPSHOT } from '../types'; import { HCI, VOLUME_SNAPSHOT } from '../types';
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass'; import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
import { DATA_ENGINE_V2 } from '../models/harvester/persistentvolumeclaim'; import { DATA_ENGINE_V2 } from '../models/harvester/persistentvolumeclaim';
import { GIBIBYTE } from '../utils/unit';
import { VOLUME_MODE } from '@pkg/harvester/config/types';
import { isInternalStorageClass } from '../utils/storage-class';
export default { export default {
name: 'HarvesterVolume', name: 'HarvesterVolume',
@@ -75,8 +72,7 @@ export default {
data() { data() {
if (this.mode === _CREATE) { if (this.mode === _CREATE) {
// default volumeMode to Block this.value.spec.volumeMode = 'Block';
this.value.spec.volumeMode = VOLUME_MODE.BLOCK;
this.value.spec.accessModes = ['ReadWriteMany']; this.value.spec.accessModes = ['ReadWriteMany'];
} }
@@ -90,7 +86,6 @@ export default {
imageId, imageId,
snapshots: [], snapshots: [],
images: [], images: [],
GIBIBYTE
}; };
}, },
@@ -129,10 +124,6 @@ export default {
return InterfaceOption; return InterfaceOption;
}, },
volumeModeOptions() {
return Object.values(VOLUME_MODE);
},
imageOption() { imageOption() {
return sortBy( return sortBy(
this.images this.images
@@ -186,39 +177,13 @@ export default {
return this.$store.getters[`${ inStore }/all`](STORAGE_CLASS); return this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
}, },
isLonghornStorageClass() {
const selectedSC = this.storageClasses.find((sc) => sc.name === this.value?.spec?.storageClassName) || {};
return selectedSC && selectedSC.isLonghorn;
},
showVolumeMode() {
// we won't let user choose volumeMode if source = vmimage
if (!this.value.thirdPartyStorageFeatureEnabled || this.isVMImage || !!this.value?.spec?.storageClassName === false) {
return false;
}
if (this.isLonghornStorageClass) {
return false;
}
return true;
},
storageClassOptions() { storageClassOptions() {
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => { return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name; const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
let disabled = false;
if (isInternalStorageClass(s.name)) {
label += ` (${ this.t('harvester.storage.internal.label') })`;
disabled = true;
}
return { return {
label, label,
value: s.name, value: s.name,
disabled
}; };
}) || []; }) || [];
}, },
@@ -270,59 +235,19 @@ export default {
} }
}, },
watch: {
'source'(neu) {
if (neu === 'url') {
this.setBlockVolumeMode();
this.deleteVolumeForVmAnnotation();
}
},
'value.spec.storageClassName'() {
if (this.isLonghornStorageClass) {
this.setBlockVolumeMode();
this.deleteVolumeForVmAnnotation();
}
},
'value.spec.volumeMode'(neu) {
if (neu === VOLUME_MODE.FILE_SYSTEM) {
this.setVolumeForVmAnnotation();
} else if (neu === VOLUME_MODE.BLOCK ) {
this.deleteVolumeForVmAnnotation();
}
}
},
methods: { methods: {
setBlockVolumeMode() {
this.value.spec.volumeMode = VOLUME_MODE.BLOCK;
},
setVolumeForVmAnnotation() {
this.value.setAnnotation(HCI_ANNOTATIONS.VOLUME_FOR_VM, 'true');
},
deleteVolumeForVmAnnotation() {
if (this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_FOR_VM]) {
delete this.value.metadata.annotations[HCI_ANNOTATIONS.VOLUME_FOR_VM];
}
},
getAccessMode() { getAccessMode() {
if (!this.longhornV2LVMSupport) { if (!this.longhornV2LVMSupport) {
return ['ReadWriteMany']; return ['ReadWriteMany'];
} }
if (this.value?.spec?.accessModes && this.value?.spec?.accessModes?.length > 0) {
return this.value.spec.accessModes;
}
const storageClassName = this.value.spec.storageClassName; const storageClassName = this.value.spec.storageClassName;
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName); const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
let readWriteOnce = this.value.isLvm || (!this.value.thirdPartyStorageFeatureEnabled && this.value.isLonghornV2); let readWriteOnce = this.value.isLvm || this.value.isLonghornV2;
if (storageClass) { if (storageClass) {
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.value.thirdPartyStorageFeatureEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2); readWriteOnce = storageClass.provisioner === LVM_DRIVER || storageClass.parameters?.dataEngine === DATA_ENGINE_V2;
} }
return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany']; return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany'];
@@ -363,7 +288,7 @@ export default {
const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize); const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize);
if (imageSize) { if (imageSize) {
this.storage = `${ Math.ceil(imageSize / 1024 / 1024 / 1024) }${ GIBIBYTE }`; this.storage = `${ Math.ceil(imageSize / 1024 / 1024 / 1024) }Gi`;
} }
} }
this.update(); this.update();
@@ -386,12 +311,10 @@ export default {
:generate-yaml="generateYaml" :generate-yaml="generateYaml"
:apply-hooks="applyHooks" :apply-hooks="applyHooks"
@finish="save" @finish="save"
@error="e=>errors=e"
> >
<NameNsDescription <NameNsDescription
:value="value" :value="value"
:namespaced="true" :namespaced="true"
:name-required="false"
:mode="mode" :mode="mode"
@update:value="$emit('update:value', $event)" @update:value="$emit('update:value', $event)"
/> />
@@ -434,7 +357,7 @@ export default {
/> />
<LabeledSelect <LabeledSelect
v-if="isBlank" v-if="source === 'blank'"
v-model:value="value.spec.storageClassName" v-model:value="value.spec.storageClassName"
:options="storageClassOptions" :options="storageClassOptions"
:label="t('harvester.storage.storageClass.label')" :label="t('harvester.storage.storageClass.label')"
@@ -444,18 +367,6 @@ export default {
@update:value="update" @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 <UnitInput
v-model:value="storage" v-model:value="storage"
:label="t('harvester.volume.size')" :label="t('harvester.volume.size')"
@@ -466,7 +377,6 @@ export default {
:disabled="value?.isLonghornV2 && isEdit" :disabled="value?.isLonghornV2 && isEdit"
required required
class="mb-20" class="mb-20"
:suffix="GIBIBYTE"
@update:value="update" @update:value="update"
/> />
@@ -64,7 +64,6 @@ export default {
:errors="errors" :errors="errors"
:apply-hooks="applyHooks" :apply-hooks="applyHooks"
@finish="save" @finish="save"
@error="e=>errors=e"
> >
<NameNsDescription <NameNsDescription
:value="value" :value="value"
@@ -1,323 +0,0 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
import Tab from '@shell/components/Tabbed/Tab';
import { NETWORK_ATTACHMENT } from '@shell/config/types';
import Loading from '@shell/components/Loading';
import CreateEditView from '@shell/mixins/create-edit-view';
import { RadioGroup } from '@components/Form/Radio';
import { NETWORK_PROTOCOL, NETWORK_TYPE } from '@pkg/harvester/config/types';
import { set } from '@shell/utils/object';
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';
export default {
name: 'EditSubnet',
emits: ['update:value'],
components: {
CruResource,
LabeledInput,
LabeledSelect,
NameNsDescription,
Tab,
RadioGroup,
ArrayList,
ResourceTabs,
Loading,
},
mixins: [CreateEditView],
inheritAttrs: false,
created() {
const vpc = this.$route.query.vpc || '';
set(this.value, 'spec', this.value.spec || {
cidrBlock: '',
protocol: NETWORK_PROTOCOL.IPv4,
provider: '',
vpc,
gatewayIP: '',
excludeIps: [],
private: false
});
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = {
vpc: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
nad: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }),
};
await allHash(hash);
},
computed: {
showAllowSubnets() {
return this.value?.spec?.private === true;
},
doneLocationOverride() {
return this.value.doneOverride;
},
allowSubnetTooltip() {
return this.t('harvester.subnet.allowSubnet.tooltip', null, true);
},
excludeIPsTooltip() {
return this.t('harvester.subnet.excludeIPs.tooltip', null, true);
},
protocolOptions() {
return Object.values(NETWORK_PROTOCOL);
},
provider: {
get() {
const raw = this.value.spec.provider;
if (!raw) {
return '';
}
const vmNet = raw.split('.')[0] || '';
const ns = raw.split('.')[1] || '';
return `${ ns }/${ vmNet }`;
},
set(value) {
const ns = value.split('/')[0] || '';
const vmNet = value.split('/')[1] || '';
const provider = `${ vmNet }.${ ns }.ovn`;
set(this.value, 'spec.provider', provider);
}
},
providerOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const vmNets = this.$store.getters[`${ inStore }/all`](NETWORK_ATTACHMENT) || [];
return vmNets.filter((net) => net.vlanType === NETWORK_TYPE.OVERLAY).map((n) => ({
label: n.id,
value: n.id,
}));
},
vpcOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const vpcs = this.$store.getters[`${ inStore }/all`](HCI.VPC) || [];
return vpcs.map((n) => ({
label: n.id,
value: n.id,
}));
}
},
methods: {
async saveSubnet(buttonCb) {
const errors = [];
const name = this.value?.metadata?.name;
try {
if (!name) {
errors.push(this.t('validation.required', { key: this.t('generic.name') }, true));
} else if (!this.value?.spec?.cidrBlock) {
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.cidrBlock.label') }, true));
} else if (!this.value?.spec?.provider) {
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'));
}
if (errors.length > 0) {
buttonCb(false);
this.errors = errors;
return false;
}
await this.value.save();
buttonCb(true);
this.done();
} catch (e) {
this.errors = [e];
buttonCb(false);
}
},
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<CruResource
v-else
:done-route="doneRoute"
:resource="value"
:mode="mode"
:apply-hooks="applyHooks"
:errors="errors"
@finish="saveSubnet"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:need-events="false"
:need-related="false"
:mode="mode"
:side-tabs="true"
>
<Tab
name="Basic"
:label="t('generic.basic')"
:weight="-1"
class="bordered-table"
>
<div class="row mt-10">
<div class="col span-6">
<LabeledInput
v-model:value="value.spec.cidrBlock"
class="mb-20"
required
:placeholder="t('harvester.subnet.cidrBlock.placeholder')"
:label="t('harvester.subnet.cidrBlock.label')"
:mode="mode"
/>
</div>
<div class="col span-6">
<LabeledSelect
v-model:value="value.spec.protocol"
:label="t('harvester.subnet.protocol.label')"
:options="protocolOptions"
required
:mode="mode"
/>
</div>
</div>
<div class="row mt-10">
<div class="col span-6">
<LabeledSelect
v-model:value="provider"
:label="t('harvester.subnet.provider.label')"
:options="providerOptions"
:tooltip="t('harvester.subnet.provider.tooltip')"
required
:mode="mode"
/>
</div>
<div class="col span-6">
<LabeledSelect
v-model:value="value.spec.vpc"
:label="t('harvester.subnet.vpc.label')"
:options="vpcOptions"
required
:disabled="true"
:mode="mode"
/>
</div>
</div>
<div class="row mt-20">
<div class="col span-6">
<LabeledInput
v-model:value="value.spec.gateway"
class="mb-20"
:placeholder="t('harvester.subnet.gateway.placeholder')"
:label="t('harvester.subnet.gateway.label')"
:mode="mode"
/>
</div>
</div>
<div class="row mt-20">
<div class="col span-6">
<RadioGroup
v-model:value="value.spec.private"
name="enabled"
:options="[true, false]"
:label="t('harvester.subnet.private.label')"
:labels="[t('generic.enabled'), t('generic.disabled')]"
:mode="mode"
tooltip-key="harvester.subnet.private.tooltip"
/>
</div>
</div>
<ArrayList
v-if="showAllowSubnets"
v-model:value="value.spec.allowSubnets"
:show-header="true"
class="mt-20"
:mode="mode"
:add-label="t('harvester.subnet.allowSubnet.addSubnet')"
>
<template #column-headers>
<div class="box">
<h3 class="key">
{{ t('harvester.subnet.allowSubnet.label') }}
<i
v-clean-tooltip="{content: allowSubnetTooltip, triggers: ['hover', 'touch', 'focus'] }"
v-stripped-aria-label="allowSubnetTooltip"
class="icon icon-info"
tabindex="0"
/>
</h3>
</div>
</template>
<template #columns="scope">
<div class="key">
<input
v-model="scope.row.value"
:placeholder="t('harvester.subnet.allowSubnet.placeholder')"
/>
</div>
</template>
</ArrayList>
<ArrayList
v-model:value="value.spec.excludeIps"
:show-header="true"
class="mt-20"
:mode="mode"
:add-label="t('harvester.setting.storageNetwork.exclude.addIp')"
>
<template #column-headers>
<div class="box">
<h3 class="key">
{{ t('harvester.setting.storageNetwork.exclude.label') }}
<i
v-clean-tooltip="{content: excludeIPsTooltip, triggers: ['hover', 'touch', 'focus'] }"
v-stripped-aria-label="excludeIPsTooltip"
class="icon icon-info"
tabindex="0"
/>
</h3>
</div>
</template>
<template #columns="scope">
<div class="key">
<input
v-model="scope.row.value"
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
/>
</div>
</template>
</ArrayList>
</Tab>
</ResourceTabs>
</CruResource>
</template>
@@ -1,162 +0,0 @@
<script>
import debounce from 'lodash/debounce';
import { _EDIT, _VIEW } from '@shell/config/query-params';
import { removeAt } from '@shell/utils/array';
export default {
name: 'StaticRoutes',
emits: ['update:value'],
props: {
value: {
type: Array,
default: null,
},
mode: {
type: String,
default: _EDIT,
},
},
data() {
const rows = (this.value || []).map((row) => {
return {
cidr: row.cidr || '',
nextHopIP: row.nextHopIP || '',
};
});
return { rows };
},
computed: {
isView() {
return this.mode === _VIEW;
},
showAdd() {
return !this.isView;
},
showRemove() {
return !this.isView;
},
nextHopIPTooltip() {
return this.t('harvester.vpc.staticRoutes.nextHopIP.tooltip');
},
},
created() {
this.queueUpdate = debounce(this.update, 100);
},
methods: {
add() {
this.rows.push({
cidr: '',
nextHopIP: '',
});
this.queueUpdate();
},
remove(idx) {
removeAt(this.rows, idx);
this.queueUpdate();
},
update() {
if (this.isView) {
return;
}
this.$emit('update:value', this.rows);
}
},
};
</script>
<template>
<div>
<div
v-if="rows.length"
class="static-route-row"
>
<div
v-for="(row, idx) in rows"
:key="idx"
>
<div class="pool-headers cidr">
<span class="pool-cidr">
<t k="harvester.vpc.staticRoutes.cidr.label" />
</span>
<span class="pool-nextHopIP">
<t k="harvester.vpc.staticRoutes.nextHopIP.label" />
<i
v-clean-tooltip="{content: nextHopIPTooltip, triggers: ['hover', 'touch', 'focus'] }"
v-stripped-aria-label="nextHopIPTooltip"
class="icon icon-info"
tabindex="0"
/>
</span>
</div>
<div class="pool-row cidr">
<div class="pool-cidr">
<span v-if="isView">
{{ row.cidr }}
</span>
<input
v-else
v-model="row.cidr"
type="text"
:placeholder="t('harvester.vpc.staticRoutes.cidr.placeholder')"
@input="queueUpdate"
/>
</div>
<div class="pool-nextHopIP">
<span v-if="isView">
{{ row.nextHopIP }}
</span>
<input
v-else
v-model="row.nextHopIP"
type="text"
:placeholder="t('harvester.vpc.staticRoutes.nextHopIP.placeholder')"
@input="queueUpdate"
/>
</div>
<button
v-if="showRemove"
type="button"
class="btn role-link pl-0"
@click="remove(idx)"
>
<t k="generic.remove" />
</button>
</div>
</div>
</div>
<button
v-if="showAdd"
type="button"
class="btn role-tertiary add"
@click="add()"
>
<t k="generic.add" />
</button>
</div>
</template>
<style lang="scss" scoped>
.pool-headers, .pool-row {
display: grid;
grid-column-gap: $column-gutter;
margin-bottom: 10px;
align-items: center;
&.cidr {
grid-template-columns: 40%+$column-gutter 40%+$column-gutter 15%;
}
}
</style>
@@ -1,187 +0,0 @@
<script>
import debounce from 'lodash/debounce';
import { _EDIT, _VIEW } from '@shell/config/query-params';
import { removeAt } from '@shell/utils/array';
import { HCI } from '../../types';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default {
name: 'VpcPeerings',
emits: ['update:value'],
components: { LabeledSelect },
props: {
value: {
type: Array,
default: null,
},
mode: {
type: String,
default: _EDIT,
},
vpc: {
type: Object,
default: () => ({}),
},
},
async fetch() {
await this.$store.dispatch('harvester/findAll', { type: HCI.VPC });
},
data() {
const rows = (this.value || []).map((row) => {
return {
localConnectIP: row.localConnectIP || '',
remoteVpc: row.remoteVpc || '',
};
});
return { rows };
},
computed: {
isView() {
return this.mode === _VIEW;
},
isEdit() {
return this.mode === _EDIT;
},
showAdd() {
return !this.isView;
},
showRemove() {
return !this.isView;
},
remoteVpcOptions() {
const allVpcs = this.$store.getters['harvester/all'](HCI.VPC) || [];
// filter self vpc if editing
const vpcs = this.isEdit ? allVpcs.filter((v) => v.id !== this.vpc.id) : allVpcs;
return vpcs.map((n) => ({
label: n.id,
value: n.id,
}));
},
localConnectIPTooltip() {
return this.t('harvester.vpc.vpcPeerings.localConnectIP.tooltip');
},
},
created() {
this.queueUpdate = debounce(this.update, 100);
},
methods: {
add() {
this.rows.push({
localConnectIP: '',
remoteVpc: '',
});
this.queueUpdate();
},
remove(idx) {
removeAt(this.rows, idx);
this.queueUpdate();
},
update() {
if (this.isView) {
return;
}
this.$emit('update:value', this.rows);
}
},
};
</script>
<template>
<div>
<div
v-if="rows.length"
class="static-route-row"
>
<div
v-for="(row, idx) in rows"
:key="idx"
>
<div class="pool-headers localConnectIP">
<span class="pool-localConnectIP">
<t k="harvester.vpc.vpcPeerings.localConnectIP.label" />
<i
v-clean-tooltip="{content: localConnectIPTooltip, triggers: ['hover', 'touch', 'focus'] }"
v-stripped-aria-label="localConnectPTooltip"
class="icon icon-info"
tabindex="0"
/>
</span>
<span class="pool-remoteVpc">
<t k="harvester.vpc.vpcPeerings.remoteVpc.label" />
</span>
</div>
<div class="pool-row localConnectIP">
<div class="pool-localConnectIP">
<span v-if="isView">
{{ row.localConnectIP }}
</span>
<input
v-else
v-model="row.localConnectIP"
type="text"
:placeholder="t('harvester.vpc.vpcPeerings.localConnectIP.placeholder')"
@input="queueUpdate"
/>
</div>
<div class="pool-remoteVpc">
<span v-if="isView">
{{ row.remoteVpc }}
</span>
<LabeledSelect
v-model:value="row.remoteVpc"
:options="remoteVpcOptions"
:mode="mode"
@update:value="queueUpdate"
/>
</div>
<button
v-if="showRemove"
type="button"
class="btn role-link pl-0"
@click="remove(idx)"
>
<t k="generic.remove" />
</button>
</div>
</div>
</div>
<button
v-if="showAdd"
type="button"
class="btn role-tertiary add"
@click="add()"
>
<t k="generic.add" />
</button>
</div>
</template>
<style lang="scss" scoped>
.pool-headers, .pool-row {
display: grid;
grid-column-gap: $column-gutter;
margin-bottom: 10px;
align-items: center;
&.localConnectIP {
grid-template-columns: 40%+$column-gutter 40%+$column-gutter 15%;
}
}
</style>
-120
View File
@@ -1,120 +0,0 @@
<script>
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import Tab from '@shell/components/Tabbed/Tab';
import Loading from '@shell/components/Loading';
import CreateEditView from '@shell/mixins/create-edit-view';
import ResourceTabs from '@shell/components/form/ResourceTabs/index';
import StaticRoutes from './StaticRoutes';
import VpcPeerings from './VpcPeerings';
import { set } from '@shell/utils/object';
export default {
name: 'EditVPC',
emits: ['update:value'],
components: {
CruResource,
NameNsDescription,
Tab,
StaticRoutes,
ResourceTabs,
Loading,
VpcPeerings
},
mixins: [CreateEditView],
inheritAttrs: false,
data() {
set(this.value, 'spec', this.value.spec || {
staticRoutes: [],
vpcPeerings: [],
});
return { staticRoutes: [] };
},
methods: {
async saveVpc(buttonCb) {
const errors = [];
try {
const name = this.value?.metadata?.name;
if (!name) {
errors.push(this.t('validation.required', { key: this.t('generic.name') }, true));
}
if (errors.length > 0) {
buttonCb(false);
this.errors = errors;
return false;
}
await this.value.save();
buttonCb(true);
this.done();
} catch (e) {
this.errors = [e];
buttonCb(false);
}
},
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<CruResource
v-else
:done-route="doneRoute"
:resource="value"
:mode="mode"
:apply-hooks="applyHooks"
:errors="errors"
@finish="saveVpc"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
:mode="mode"
:namespaced="false"
@update:value="$emit('update:value', $event)"
/>
<ResourceTabs
class="mt-15"
:mode="mode"
:side-tabs="true"
>
<Tab
name="staticRoutes"
:label="t('harvester.vpc.staticRoutes.label')"
:weight="-1"
class="bordered-table"
>
<StaticRoutes
v-model:value="value.spec.staticRoutes"
class="col span-12"
:mode="mode"
/>
</Tab>
<Tab
name="vpcPeerings"
:label="t('harvester.vpc.vpcPeerings.label')"
:weight="-2"
class="bordered-table"
>
<VpcPeerings
v-model:value="value.spec.vpcPeerings"
class="col span-12"
:mode="mode"
:vpc="value"
/>
</Tab>
</ResourceTabs>
</CruResource>
</template>
@@ -20,10 +20,6 @@ export default {
type: String, type: String,
default: 'create' default: 'create'
}, },
osType: {
type: String,
default: ''
},
value: { value: {
type: String, type: String,
default: '' default: ''
@@ -68,12 +64,6 @@ export default {
}, },
watch: { watch: {
osType(neu) {
if (neu === 'windows') {
this.id = '';
}
},
value(neu) { value(neu) {
this.yamlScript = neu; this.yamlScript = neu;
}, },
@@ -28,10 +28,6 @@ export default {
type: String, type: String,
default: '' default: ''
}, },
osType: {
type: String,
default: ''
},
userScript: { userScript: {
type: String, type: String,
default: '' default: ''
@@ -207,7 +203,6 @@ export default {
<DataTemplate <DataTemplate
ref="userTemplate" ref="userTemplate"
:value="userScript" :value="userScript"
:os-type="osType"
type="user" type="user"
:view-code="viewCode" :view-code="viewCode"
:mode="mode" :mode="mode"
@@ -224,7 +219,6 @@ export default {
ref="networkTemplate" ref="networkTemplate"
:value="networkScript" :value="networkScript"
type="network" type="network"
:os-type="osType"
:view-code="viewCode" :view-code="viewCode"
:mode="mode" :mode="mode"
:config-id="configNetworkId" :config-id="configNetworkId"
@@ -1,45 +1,23 @@
<script> <script>
import UnitInput from '@shell/components/form/UnitInput'; import UnitInput from '@shell/components/form/UnitInput';
import InputOrDisplay from '@shell/components/InputOrDisplay'; import InputOrDisplay from '@shell/components/InputOrDisplay';
import { GIBIBYTE } from '../../utils/unit';
import { Checkbox } from '@components/Form/Checkbox';
import { _VIEW } from '@shell/config/query-params';
import { HCI } from '../../types';
import { allHash } from '@shell/utils/promise';
import { HCI_SETTING } from '../../config/settings';
const DEFAULT_HOT_PLUG_TIMES = 4;
export default { export default {
name: 'HarvesterEditCpuMemory', name: 'HarvesterEditCpuMemory',
emits: ['updateCpuMemory'], emits: ['updateCpuMemory'],
components: { components: { UnitInput, InputOrDisplay },
UnitInput, InputOrDisplay, Checkbox
},
props: { props: {
cpu: { cpu: {
type: Number, type: Number,
default: null default: null
}, },
maxCpu: {
type: Number,
default: null
},
memory: { memory: {
type: String, type: String,
default: null default: null
}, },
maxMemory: {
type: String,
default: null
},
enableHotPlug: {
type: Boolean,
default: false
},
mode: { mode: {
type: String, type: String,
default: 'create', default: 'create',
@@ -50,59 +28,20 @@ export default {
} }
}, },
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const res = await allHash({ settings: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }) });
this.settings = res.settings || [];
},
data() { data() {
return { return {
GIBIBYTE,
localCpu: this.cpu, localCpu: this.cpu,
localMemory: this.memory, localMemory: this.memory
maxLocalCpu: this.maxCpu,
maxLocalMemory: this.maxMemory,
localEnableHotPlug: this.enableHotPlug,
settings: []
}; };
}, },
computed: { computed: {
isView() { cupDisplay() {
return this.mode === _VIEW;
},
cpuDisplay() {
return `${ this.localCpu } C`; return `${ this.localCpu } C`;
}, },
maxCpuDisplay() {
return `${ this.maxLocalCpu } C`;
},
memoryDisplay() { memoryDisplay() {
return `${ this.localMemory }`; return `${ this.localMemory }`;
},
maxMemoryDisplay() {
return `${ this.maxLocalMemory }`;
},
cpuMemoryHotplugTooltip() {
return this.t('harvester.virtualMachine.hotplug.tooltip', { hotPlugTimes: this.maxHotplugRatio });
},
isCPUMemoryHotPlugFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('cpuMemoryHotplug');
},
maxHotplugRatio() {
const maxHotPlugRatioSetting = this.settings.find((s) => s.id === HCI_SETTING.MAX_HOTPLUG_RATIO);
const maxPlugRatio = maxHotPlugRatioSetting?.value ? parseInt(maxHotPlugRatioSetting.value, 10) : DEFAULT_HOT_PLUG_TIMES;
return maxPlugRatio;
} }
}, },
@@ -114,60 +53,35 @@ export default {
if (neu && !neu.includes('null')) { if (neu && !neu.includes('null')) {
this.localMemory = neu; this.localMemory = neu;
} }
},
maxCpu(neu) {
this.maxLocalCpu = neu;
},
maxMemory(neu) {
if (neu && !neu.includes('null')) {
this.maxLocalMemory = neu;
} }
}, },
enableHotPlug(neu) {
this.localEnableHotPlug = neu;
},
},
methods: { methods: {
hotPlugChanged(neu) { change() {
// If hot plug is enabled, we need to update the maxCpu and maxMemory values let memory = '';
if (neu) {
this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null; if (String(this.localMemory).includes('Gi')) {
this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null; memory = this.localMemory;
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, neu);
} else { } else {
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, '', null, neu); memory = `${ this.localMemory }Gi`;
} }
}, if (memory.includes('null')) {
changeMemory() { memory = null;
if (this.localEnableHotPlug) {
this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null;
} }
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug); this.$emit('updateCpuMemory', this.localCpu, memory);
},
changeCPU() {
if (this.localEnableHotPlug) {
this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null;
}
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
},
changeMaxCPUMemory() {
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
}, },
} }
}; };
</script> </script>
<template> <template>
<div>
<div class="row"> <div class="row">
<div class="col span-6 mb-10"> <div class="col span-6 mb-10">
<InputOrDisplay <InputOrDisplay
name="CPU" name="CPU"
:value="cpuDisplay" :value="cupDisplay"
:mode="mode" :mode="mode"
class="mb-10"
> >
<UnitInput <UnitInput
v-model:value="localCpu" v-model:value="localCpu"
@@ -178,10 +92,11 @@ export default {
:disabled="disabled" :disabled="disabled"
:mode="mode" :mode="mode"
class="mb-20" class="mb-20"
@update:value="changeCPU" @update:value="change"
/> />
</InputOrDisplay> </InputOrDisplay>
</div> </div>
<div class="col span-6 mb-10"> <div class="col span-6 mb-10">
<InputOrDisplay <InputOrDisplay
:name="t('harvester.virtualMachine.input.memory')" :name="t('harvester.virtualMachine.input.memory')"
@@ -198,77 +113,10 @@ export default {
:output-modifier="true" :output-modifier="true"
:disabled="disabled" :disabled="disabled"
required required
:suffix="GIBIBYTE"
class="mb-20" class="mb-20"
@update:value="changeMemory" @update:value="change"
/> />
</InputOrDisplay> </InputOrDisplay>
</div> </div>
</div> </div>
<div
v-if="isCPUMemoryHotPlugFeatureEnabled"
class="row"
>
<Checkbox
v-model:value="localEnableHotPlug"
class="check"
type="checkbox"
:label="t('harvester.virtualMachine.hotplug.title')"
:disabled="isView"
@update:value="hotPlugChanged"
/>
<i
v-clean-tooltip="{content: cpuMemoryHotplugTooltip, triggers: ['hover', 'touch', 'focus'] }"
v-stripped-aria-label="cpuMemoryHotplugTooltip"
class="icon icon-info"
tabindex="0"
/>
</div>
<div
v-if="localEnableHotPlug && isCPUMemoryHotPlugFeatureEnabled"
class="row"
>
<div class="col span-6 mb-10">
<InputOrDisplay
:name="t('harvester.virtualMachine.input.maxCpu')"
:value="maxCpuDisplay"
:mode="mode"
class="mt-20"
>
<UnitInput
v-model:value="maxLocalCpu"
:label="t('harvester.virtualMachine.input.maxCpu')"
suffix="C"
:delay="0"
:disabled="disabled"
:mode="mode"
class="mt-20"
@update:value="changeMaxCPUMemory"
/>
</InputOrDisplay>
</div>
<div class="col span-6 mb-10">
<InputOrDisplay
:name="t('harvester.virtualMachine.input.maxMemory')"
:value="maxMemoryDisplay"
:mode="mode"
class="mt-20"
>
<UnitInput
v-model:value="maxLocalMemory"
:label="t('harvester.virtualMachine.input.maxMemory')"
:mode="mode"
:input-exponent="3"
:delay="0"
:increment="1024"
:output-modifier="true"
:disabled="disabled"
:suffix="GIBIBYTE"
class="mt-20"
@update:value="changeMaxCPUMemory"
/>
</InputOrDisplay>
</div>
</div>
</div>
</template> </template>
@@ -97,7 +97,7 @@ export default {
}); });
const devices = [ const devices = [
...this.otherDevices(this.value?.domain?.devices?.hostDevices || []), ...this.otherDevices(this.value.domain.devices.hostDevices || []),
...formatted, ...formatted,
]; ];
@@ -121,9 +121,7 @@ export default {
return inUse; return inUse;
} }
const hostDevices = vm?.hostDevices || []; vm.hostDevices.forEach((device) => {
hostDevices.forEach((device) => {
inUse[device.name] = { usedBy: [vm.metadata.name] }; inUse[device.name] = { usedBy: [vm.metadata.name] };
}); });
@@ -1,6 +1,6 @@
<script> <script>
import UnitInput from '@shell/components/form/UnitInput'; import UnitInput from '@shell/components/form/UnitInput';
import { MEBIBYTE } from '../../utils/unit';
export default { export default {
name: 'HarvesterReserved', name: 'HarvesterReserved',
@@ -20,7 +20,7 @@ export default {
}, },
data() { data() {
return { MEBIBYTE, memory: this.reservedMemory }; return { memory: this.reservedMemory };
}, },
watch: { watch: {
@@ -47,7 +47,6 @@ export default {
:input-exponent="2" :input-exponent="2"
:increment="1024" :increment="1024"
:output-modifier="true" :output-modifier="true"
:suffix="MEBIBYTE"
@update:value="change" @update:value="change"
/> />
</template> </template>
@@ -1,13 +1,11 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { randomStr } from '@shell/utils/string'; import { randomStr } from '@shell/utils/string';
import { clone } from '@shell/utils/object';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import ModalWithCard from '@shell/components/ModalWithCard'; import ModalWithCard from '@shell/components/ModalWithCard';
import { _VIEW } from '@shell/config/query-params'; import { _VIEW } from '@shell/config/query-params';
import { NAMESPACE } from '@shell/config/types'; import { NAMESPACE } from '@shell/config/types';
import { HCI } from '../../types'; import { HCI } from '../../types';
@@ -128,6 +126,7 @@ export default {
checkedSsh(val) { checkedSsh(val) {
// if click on Create a New... // if click on Create a New...
if (val.includes(_NEW)) { if (val.includes(_NEW)) {
this.checkedSsh = this.checkedSsh.filter((key) => key !== _NEW);
this.show(); this.show();
} }
} }
@@ -231,9 +230,7 @@ export default {
}, },
update() { update() {
const sshKeys = this.checkedSsh.filter((key) => key !== _NEW); this.$emit('update:sshKey', clone(this.checkedSsh));
this.$emit('update:sshKey', sshKeys);
}, },
} }
}; };
@@ -80,7 +80,7 @@ export default {
}); });
const devices = [ const devices = [
...this.otherDevices(this.value?.domain?.devices?.hostDevices || []), ...this.otherDevices(this.value.domain.devices.hostDevices || []),
...formatted, ...formatted,
]; ];
@@ -121,9 +121,7 @@ export default {
return inUse; return inUse;
} }
const hostDevices = vm?.hostDevices || []; vm.hostDevices.forEach((device) => {
hostDevices.forEach((device) => {
inUse[device.name] = { usedBy: [vm.metadata.name] }; inUse[device.name] = { usedBy: [vm.metadata.name] };
}); });
@@ -7,7 +7,8 @@ import UnitInput from '@shell/components/form/UnitInput';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import ModalWithCard from '@shell/components/ModalWithCard'; import ModalWithCard from '@shell/components/ModalWithCard';
import { PVC, STORAGE_CLASS } from '@shell/config/types';
import { PVC } from '@shell/config/types';
import { clone } from '@shell/utils/object'; import { clone } from '@shell/utils/object';
import { ucFirst, randomStr } from '@shell/utils/string'; import { ucFirst, randomStr } from '@shell/utils/string';
import { removeObject } from '@shell/utils/array'; import { removeObject } from '@shell/utils/array';
@@ -16,8 +17,6 @@ import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
import { SOURCE_TYPE } from '../../../config/harvester-map'; import { SOURCE_TYPE } from '../../../config/harvester-map';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester'; import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
import { HCI } from '../../../types'; import { HCI } from '../../../types';
import { VOLUME_MODE } from '@pkg/harvester/config/types';
import { OFF } from '../../../models/kubevirt.io.virtualmachine';
export default { export default {
emits: ['update:value'], emits: ['update:value'],
@@ -63,7 +62,7 @@ export default {
customVolumeMode: { customVolumeMode: {
type: String, type: String,
default: VOLUME_MODE.Block default: 'Block'
}, },
customAccessMode: { customAccessMode: {
@@ -117,12 +116,6 @@ export default {
return this.mode === _CREATE; return this.mode === _CREATE;
}, },
defaultStorageClass() {
const defaultStorage = this.$store.getters['harvester/all'](STORAGE_CLASS).find((sc) => sc.isDefault);
return defaultStorage;
},
showVolumeTip() { showVolumeTip() {
const imageName = this.getImageDisplayName(this.rows[0]?.image); const imageName = this.getImageDisplayName(this.rows[0]?.image);
@@ -267,17 +260,7 @@ export default {
isLonghornV2(volume) { isLonghornV2(volume) {
return volume?.pvc?.isLonghornV2 || volume?.pvc?.storageClass?.isLonghornV2; return volume?.pvc?.isLonghornV2 || volume?.pvc?.storageClass?.isLonghornV2;
}, }
isResizeDisabled(volume) {
if (this.isCreate) return false;
if (volume.newCreateId) return false;
const isStopped = this.vm.stateDisplay === OFF;
const isLonghornV2 = this.isLonghornV2(volume);
return !isStopped || isLonghornV2;
},
}, },
}; };
</script> </script>
@@ -358,7 +341,6 @@ export default {
:is-edit="isEdit" :is-edit="isEdit"
:is-view="isView" :is-view="isView"
:is-virtual-type="isVirtualType" :is-virtual-type="isVirtualType"
:is-resize-disabled="isResizeDisabled(rows[i])"
:mode="mode" :mode="mode"
:idx="i" :idx="i"
:validate-required="validateRequired" :validate-required="validateRequired"
@@ -11,7 +11,6 @@ import { _CREATE } from '@shell/config/query-params';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { HCI } from '../../../../types'; import { HCI } from '../../../../types';
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map'; import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
import { GIBIBYTE } from '../../../../utils/unit';
export default { export default {
name: 'HarvesterEditExisting', name: 'HarvesterEditExisting',
@@ -60,7 +59,6 @@ export default {
} }
return { return {
GIBIBYTE,
VOLUME_TYPE, VOLUME_TYPE,
InterfaceOption, InterfaceOption,
loading: false, loading: false,
@@ -111,10 +109,6 @@ export default {
return false; return false;
} }
if (pvc.isGoldenImageVolume) {
return false;
}
if (pvc.attachVM && isAvailable && pvc.attachVM?.id === this.vm?.id && this.isEdit) { if (pvc.attachVM && isAvailable && pvc.attachVM?.id === this.vm?.id && this.isEdit) {
isBeingUsed = false; isBeingUsed = false;
} else if (pvc.attachVM) { } else if (pvc.attachVM) {
@@ -142,12 +136,10 @@ export default {
return; return;
} }
// update this.value with existing volume spec, then update to upstream component
this.value.accessModes = pvcResource.spec.accessModes[0]; this.value.accessModes = pvcResource.spec.accessModes[0];
this.value.size = pvcResource.spec.resources.requests.storage; this.value.size = pvcResource.spec.resources.requests.storage;
this.value.storageClassName = pvcResource.spec.storageClassName; this.value.storageClassName = pvcResource.spec.storageClassName;
this.value.volumeMode = pvcResource.spec.volumeMode; this.value.volumeMode = pvcResource.spec.volumeMode;
this.update();
}, },
'value.type'(neu) { 'value.type'(neu) {
@@ -258,7 +250,6 @@ export default {
:label="t('harvester.fields.size')" :label="t('harvester.fields.size')"
:mode="mode" :mode="mode"
:disabled="true" :disabled="true"
:suffix="GIBIBYTE"
@update:value="update" @update:value="update"
/> />
</InputOrDisplay> </InputOrDisplay>
@@ -8,12 +8,10 @@ import { Banner } from '@components/Banner';
import { PVC } from '@shell/config/types'; import { PVC } from '@shell/config/types';
import { formatSi, parseSi } from '@shell/utils/units'; import { formatSi, parseSi } from '@shell/utils/units';
import { HCI } from '../../../../types'; import { HCI } from '../../../../types';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map'; import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
import { _VIEW } from '@shell/config/query-params'; import { _VIEW } from '@shell/config/query-params';
import LabelValue from '@shell/components/LabelValue'; import LabelValue from '@shell/components/LabelValue';
import { ucFirst } from '@shell/utils/string'; import { ucFirst } from '@shell/utils/string';
import { GIBIBYTE } from '../../../../utils/unit';
export default { export default {
name: 'HarvesterEditVMImage', name: 'HarvesterEditVMImage',
@@ -64,19 +62,12 @@ export default {
isVirtualType: { isVirtualType: {
type: Boolean, type: Boolean,
default: true default: true
}, }
isResizeDisabled: {
type: Boolean,
default: false
},
}, },
data() { data() {
return { return {
GIBIBYTE,
VOLUME_TYPE, VOLUME_TYPE,
HCI_ANNOTATIONS,
InterfaceOption, InterfaceOption,
loading: false, loading: false,
images: [], images: [],
@@ -97,23 +88,12 @@ export default {
}, },
imagesOption() { imagesOption() {
return this.images return this.images.filter((c) => c.isReady).sort((a, b) => a.creationTimestamp > b.creationTimestamp ? -1 : 1).map( (I) => {
.filter((image) => { return {
if (!image.isReady) return false; label: `${ I.metadata.namespace }/${ I.spec.displayName }`,
value: I.id
// exclude internal images created during upgrade };
const isInternalCreatedImage = });
image.namespace === 'harvester-system' &&
image.labels?.[HCI_ANNOTATIONS.UPGRADE];
return !isInternalCreatedImage;
})
.sort((a, b) => (a.creationTimestamp > b.creationTimestamp ? -1 : 1))
.map((image) => ({
label: this.imageOptionLabel(image),
value: image.id,
disabled: image.isImportedImage
}));
}, },
imageName() { imageName() {
@@ -136,42 +116,8 @@ export default {
}); });
}, },
thirdPartyStorageEnabled() { isLonghornV2() {
return this.$store.getters['harvester-common/getFeatureEnabled']('thirdPartyStorage'); return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.isLonghornV2;
},
selectedImage() {
return this.$store.getters['harvester/all'](HCI.IMAGE)?.find( (I) => this.value.image === I.id);
},
imageVirtualSize() {
if (this.selectedImage?.virtualSize) {
return this.selectedImage.virtualSize.replace(' ', '');
}
return '0';
},
diskSize() {
const size = this.value?.size || '0';
return size;
},
imageVirtualSizeInByte() {
return Math.max(this.selectedImage?.status?.size, this.selectedImage?.status?.virtualSize);
},
diskSizeInByte() {
return parseSi(this.value?.size || '0');
},
showDiskTooSmallError() {
if (!this.thirdPartyStorageEnabled ) {
return false;
}
return this.imageVirtualSizeInByte > this.diskSizeInByte;
} }
}, },
@@ -203,7 +149,7 @@ export default {
minExponent: 3, minExponent: 3,
}); });
this.value.size = `${ formatSize }${ GIBIBYTE }`; this.value.size = `${ formatSize }Gi`;
} }
}, },
deep: true, deep: true,
@@ -212,17 +158,7 @@ export default {
}, },
methods: { methods: {
imageOptionLabel(image) {
let label = `${ image.metadata.namespace }/${ image.spec.displayName }`;
if (this.thirdPartyStorageEnabled) {
label += ` (${ image.imageStorageClass } / ${ image.virtualSize })`;
}
return label;
},
update() { update() {
this.value.hasDiskError = this.showDiskTooSmallError;
this.$emit('update'); this.$emit('update');
}, },
@@ -245,7 +181,7 @@ export default {
if (!isIsoImage) { if (!isIsoImage) {
imageSizeGiB = Math.max(imageSizeGiB, 10); imageSizeGiB = Math.max(imageSizeGiB, 10);
} }
this.value['size'] = `${ imageSizeGiB }${ GIBIBYTE }`; this.value['size'] = `${ imageSizeGiB }Gi`;
} }
this.update(); this.update();
@@ -351,8 +287,8 @@ export default {
:label="t('harvester.fields.size')" :label="t('harvester.fields.size')"
:mode="mode" :mode="mode"
:required="validateRequired" :required="validateRequired"
:disabled="isResizeDisabled" :disable="isLonghornV2"
:suffix="GIBIBYTE" suffix="GiB"
@update:value="update" @update:value="update"
/> />
</InputOrDisplay> </InputOrDisplay>
@@ -405,10 +341,5 @@ export default {
class="mb-20" class="mb-20"
:label="value.volumeBackups.error.message" :label="value.volumeBackups.error.message"
/> />
<Banner
v-if="!isView && showDiskTooSmallError"
color="error"
:label="t('harvester.virtualMachine.volume.vmImageVolumeTip', {diskSize: diskSize, imageVirtualSize: imageVirtualSize})"
/>
</div> </div>
</template> </template>
@@ -12,9 +12,6 @@ import LabelValue from '@shell/components/LabelValue';
import { ucFirst } from '@shell/utils/string'; import { ucFirst } from '@shell/utils/string';
import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storageclass'; import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storageclass';
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim'; import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
import { GIBIBYTE } from '../../../../utils/unit';
import { isInternalStorageClass } from '../../../../utils/storage-class';
import { VOLUME_MODE } from '@pkg/harvester/config/types';
export default { export default {
name: 'HarvesterEditVolume', name: 'HarvesterEditVolume',
@@ -56,17 +53,11 @@ export default {
isVirtualType: { isVirtualType: {
type: Boolean, type: Boolean,
default: true default: true
}, }
isResizeDisabled: {
type: Boolean,
default: false
},
}, },
data() { data() {
return { return {
GIBIBYTE,
VOLUME_TYPE, VOLUME_TYPE,
InterfaceOption, InterfaceOption,
loading: false, loading: false,
@@ -78,10 +69,6 @@ export default {
return this.$store.getters['harvester-common/getFeatureEnabled']('longhornV2LVMSupport'); return this.$store.getters['harvester-common/getFeatureEnabled']('longhornV2LVMSupport');
}, },
thirdPartyStorageClassEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
},
encryptionValue() { encryptionValue() {
return ucFirst(String(this.value.isEncrypted)); return ucFirst(String(this.value.isEncrypted));
}, },
@@ -102,18 +89,6 @@ export default {
return allPVCs.find((P) => P.id === `${ this.namespace }/${ this.value.volumeName }`); return allPVCs.find((P) => P.id === `${ this.namespace }/${ this.value.volumeName }`);
}, },
showVolumeMode() {
if (!this.thirdPartyStorageClassEnabled || !!this.value?.storageClassName === false) {
return false;
}
if (this.isLonghornStorageClass) {
return false;
}
return true;
},
isDisabled() { isDisabled() {
return !this.value.newCreateId && this.isEdit && this.isVirtualType; return !this.value.newCreateId && this.isEdit && this.isVirtualType;
}, },
@@ -124,31 +99,18 @@ export default {
storageClassOptions() { storageClassOptions() {
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => { return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name; const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
let disabled = false;
if (isInternalStorageClass(s.name)) {
label += ` (${ this.t('harvester.storage.internal.label') })`;
disabled = true;
}
return { return {
label, label,
value: s.name, value: s.name,
disabled
}; };
}) || []; }) || [];
}, },
isLonghornStorageClass() { isLonghornV2() {
const selectedSC = this.storageClasses.find((sc) => sc.name === this.value?.storageClassName) || {}; return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.isLonghornV2;
}
return selectedSC && selectedSC.isLonghorn;
},
volumeModeOptions() {
return Object.values(VOLUME_MODE);
},
}, },
watch: { watch: {
@@ -156,8 +118,6 @@ export default {
immediate: true, immediate: true,
handler(neu) { handler(neu) {
this.value.accessMode = this.getAccessMode(neu); this.value.accessMode = this.getAccessMode(neu);
this.value.volumeMode = this.getVolumeMode(neu, this.value.volumeMode);
this.update();
} }
}, },
@@ -180,7 +140,7 @@ export default {
minExponent: 3, minExponent: 3,
}); });
this.value.size = `${ formatSize }${ GIBIBYTE }`; this.value.size = `${ formatSize }Gi`;
} }
}, },
deep: true, deep: true,
@@ -189,20 +149,6 @@ export default {
}, },
methods: { methods: {
getVolumeMode(storageClassName, originalVolumeMode) {
if (!this.thirdPartyStorageClassEnabled) {
return VOLUME_MODE.BLOCK;
}
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
// longhorn v1, v2 use block volumeMode
if (storageClass && storageClass.isLonghorn) {
return VOLUME_MODE.BLOCK;
}
return originalVolumeMode;
},
getAccessMode(storageClassName) { getAccessMode(storageClassName) {
if (!this.longhornV2LVMSupport) { if (!this.longhornV2LVMSupport) {
return 'ReadWriteMany'; return 'ReadWriteMany';
@@ -210,10 +156,10 @@ export default {
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName); const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
let readWriteOnce = this.value.pvc?.isLvm || (!this.thirdPartyStorageClassEnabled && this.value.pvc?.isLonghornV2); let readWriteOnce = this.value.pvc?.isLvm || this.value.pvc?.isLonghornV2;
if (storageClass) { if (storageClass) {
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.thirdPartyStorageClassEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2); readWriteOnce = storageClass.provisioner === LVM_DRIVER || storageClass.parameters?.dataEngine === DATA_ENGINE_V2;
} }
return readWriteOnce ? 'ReadWriteOnce' : 'ReadWriteMany'; return readWriteOnce ? 'ReadWriteOnce' : 'ReadWriteMany';
@@ -311,8 +257,7 @@ export default {
:mode="mode" :mode="mode"
:required="validateRequired" :required="validateRequired"
:label="t('harvester.fields.size')" :label="t('harvester.fields.size')"
:disabled="isResizeDisabled" :disabled="isLonghornV2"
:suffix="GIBIBYTE"
@update:value="update" @update:value="update"
/> />
</InputOrDisplay> </InputOrDisplay>
@@ -338,29 +283,6 @@ export default {
/> />
</InputOrDisplay> </InputOrDisplay>
</div> </div>
<div
v-if="showVolumeMode"
data-testid="input-volume-mode"
class="col span-6"
>
<InputOrDisplay
:name="t('harvester.volume.volumeMode')"
:value="value.volumeMode"
:mode="mode"
>
<LabeledSelect
v-model:value="value.volumeMode"
:label="t('harvester.volume.volumeMode')"
:mode="mode"
:options="volumeModeOptions"
:disabled="isEdit"
required
@update:value="update"
/>
</InputOrDisplay>
</div>
</div>
<div class="row mb-20">
<div <div
v-if="value.volumeEncryptionFeatureEnabled && isView" v-if="value.volumeEncryptionFeatureEnabled && isView"
class="col span-6" class="col span-6"
@@ -370,9 +292,11 @@ export default {
:value="encryptionValue" :value="encryptionValue"
/> />
</div> </div>
</div>
<div class="row mb-20">
<div <div
v-if="value.volumeBackups && isView" v-if="value.volumeBackups && isView"
class="col span-6" class="col span-3"
> >
<LabelValue <LabelValue
:name="t('harvester.virtualMachine.volume.readyToUse')" :name="t('harvester.virtualMachine.volume.readyToUse')"
@@ -2,7 +2,6 @@
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import Tabbed from '@shell/components/Tabbed'; import Tabbed from '@shell/components/Tabbed';
import { clone, set } from '@shell/utils/object';
import Tab from '@shell/components/Tabbed/Tab'; import Tab from '@shell/components/Tabbed/Tab';
import { Checkbox } from '@components/Form/Checkbox'; import { Checkbox } from '@components/Form/Checkbox';
import CruResource from '@shell/components/CruResource'; import CruResource from '@shell/components/CruResource';
@@ -11,12 +10,16 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import NameNsDescription from '@shell/components/form/NameNsDescription'; import NameNsDescription from '@shell/components/form/NameNsDescription';
import UnitInput from '@shell/components/form/UnitInput'; import UnitInput from '@shell/components/form/UnitInput';
import Labels from '@shell/components/form/Labels';
import NodeScheduling from '@shell/components/form/NodeScheduling'; import NodeScheduling from '@shell/components/form/NodeScheduling';
import PodAffinity from '@shell/components/form/PodAffinity'; import PodAffinity from '@shell/components/form/PodAffinity';
import VGpuDevices from './VirtualMachineVGpuDevices/index'; import VGpuDevices from './VirtualMachineVGpuDevices/index';
import UsbDevices from './VirtualMachineUSBDevices/index'; import UsbDevices from './VirtualMachineUSBDevices/index';
import KeyValue from '@shell/components/form/KeyValue'; import KeyValue from '@shell/components/form/KeyValue';
import { clear } from '@shell/utils/array'; import { clear } from '@shell/utils/array';
import { clone } from '@shell/utils/object';
import { saferDump } from '@shell/utils/create-yaml'; import { saferDump } from '@shell/utils/create-yaml';
import { exceptionToErrorsArray } from '@shell/utils/error'; import { exceptionToErrorsArray } from '@shell/utils/error';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
@@ -62,6 +65,7 @@ export default {
PodAffinity, PodAffinity,
AccessCredentials, AccessCredentials,
Reserved, Reserved,
Labels,
PciDevices, PciDevices,
RestartVMDialog, RestartVMDialog,
UnitInput, UnitInput,
@@ -114,11 +118,13 @@ export default {
}, },
machineTypeOptions() { machineTypeOptions() {
return this.machineTypes.map((type) => { return [{
if (!type) return { label: 'None', value: '' }; label: 'None',
value: ''
return { label: type, value: type }; }, {
}); label: 'q35',
value: 'q35'
}];
}, },
templateOptions() { templateOptions() {
@@ -189,12 +195,7 @@ export default {
return true; return true;
}, },
isValidationPassed() {
// check if any disk hasDiskError is true
const hasError = this.diskRows.some((disk) => disk.hasDiskError === true);
return !hasError;
},
showCpuPinningBanner() { showCpuPinningBanner() {
if (!this.value.cpuPinningFeatureEnabled) { if (!this.value.cpuPinningFeatureEnabled) {
return false; return false;
@@ -209,10 +210,7 @@ export default {
} }
return false; return false;
}, }
usbPassthroughEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
},
}, },
watch: { watch: {
@@ -237,9 +235,6 @@ export default {
} }
const versions = await this.$store.dispatch('harvester/findAll', { type: HCI.VM_VERSION }); const versions = await this.$store.dispatch('harvester/findAll', { type: HCI.VM_VERSION });
const curVersion = versions.find( (V) => V.id === id); const curVersion = versions.find( (V) => V.id === id);
if (!curVersion?.spec?.vm) return;
const cloneVersionVM = clone(curVersion.spec.vm); const cloneVersionVM = clone(curVersion.spec.vm);
delete cloneVersionVM.spec?.template?.spec?.accessCredentials; delete cloneVersionVM.spec?.template?.spec?.accessCredentials;
@@ -247,6 +242,7 @@ export default {
delete cloneVersionVM.spec?.template?.metadata?.annotations?.[HCI_ANNOTATIONS.DYNAMIC_SSHKEYS_USERS]; delete cloneVersionVM.spec?.template?.metadata?.annotations?.[HCI_ANNOTATIONS.DYNAMIC_SSHKEYS_USERS];
const claimTemplate = parseVolumeClaimTemplates(cloneVersionVM); const claimTemplate = parseVolumeClaimTemplates(cloneVersionVM);
const deleteDataSource = claimTemplate.map((volume) => { const deleteDataSource = claimTemplate.map((volume) => {
if (volume?.spec?.dataSource) { if (volume?.spec?.dataSource) {
delete volume.spec.dataSource; delete volume.spec.dataSource;
@@ -257,17 +253,6 @@ export default {
cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource); cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
// Update instance labels, labels and annotations
this.value.spec.template.metadata.labels = cloneVersionVM.spec.template.metadata.labels || {};
this.value.metadata.labels = {
...(curVersion.metadata?.labels || {}),
...(cloneVersionVM.metadata?.labels || {})
};
this.value.metadata.annotations = {
...(curVersion.metadata?.annotations || {}),
...(cloneVersionVM.metadata?.annotations || {})
};
this.getInitConfig({ this.getInitConfig({
value: cloneVersionVM, existUserData: true, fromTemplate: true value: cloneVersionVM, existUserData: true, fromTemplate: true
}); });
@@ -422,26 +407,13 @@ export default {
} }
const cloneDeepNewVM = clone(this.value); const cloneDeepNewVM = clone(this.value);
// new VM
delete cloneDeepNewVM?.metadata; delete cloneDeepNewVM?.metadata;
delete cloneDeepNewVM?.__clone;
// old VM
delete this.cloneVM?.metadata; delete this.cloneVM?.metadata;
delete this.cloneVM?.__clone; delete this.cloneVM?.__clone;
// add empty hostDevices to old VM as CRD does not have it.
const devicesObj = this.cloneVM?.spec?.template?.spec?.domain?.devices;
if (devicesObj && devicesObj.hostDevices === undefined) {
set(devicesObj, 'hostDevices', []);
}
const oldVM = JSON.parse(JSON.stringify(this.cloneVM)); const oldVM = JSON.parse(JSON.stringify(this.cloneVM));
const newVM = JSON.parse(JSON.stringify(cloneDeepNewVM)); const newVM = JSON.parse(JSON.stringify(cloneDeepNewVM));
// we won't show restart dialog in yaml page as we don't have a way to detect change in yaml editor.
// only check VM is changed in form page.
if (isEqual(oldVM, newVM)) { if (isEqual(oldVM, newVM)) {
return; return;
} }
@@ -450,11 +422,7 @@ export default {
this.isOpen = true; this.isOpen = true;
this.$nextTick(() => { this.$nextTick(() => {
if (this?.$refs?.restartDialog) {
this.$refs.restartDialog.resolve = resolve; this.$refs.restartDialog.resolve = resolve;
} else {
return resolve();
}
}); });
}); });
}, },
@@ -466,9 +434,7 @@ export default {
} }
} }
const cpuMemoryErrors = this.getCPUMemoryValidation(); const errors = this.getAccessCredentialsValidation();
const accessCredentialsErrors = this.getAccessCredentialsValidation();
const errors = [...cpuMemoryErrors, ...accessCredentialsErrors];
if (errors.length > 0) { if (errors.length > 0) {
return Promise.reject(errors); return Promise.reject(errors);
@@ -506,7 +472,6 @@ export default {
}, },
generateYaml() { generateYaml() {
this.showYaml = this?.$refs?.vmCruResource?.showYaml || false;
this.parseVM(); this.parseVM();
const out = saferDump(this.value); const out = saferDump(this.value);
@@ -520,19 +485,16 @@ export default {
<CruResource <CruResource
v-if="spec" v-if="spec"
id="vm" id="vm"
ref="vmCruResource"
:done-route="doneRoute" :done-route="doneRoute"
:resource="value" :resource="value"
:cancel-event="true" :cancel-event="true"
:mode="mode" :mode="mode"
:validation-passed="isValidationPassed"
:can-yaml="isSingle ? true : false" :can-yaml="isSingle ? true : false"
:errors="errors" :errors="errors"
:generate-yaml="generateYaml" :generate-yaml="generateYaml"
:apply-hooks="applyHooks" :apply-hooks="applyHooks"
@finish="saveVM" @finish="saveVM"
@cancel="cancelAction" @cancel="cancelAction"
@error="e=>errors=e"
> >
<RadioGroup <RadioGroup
v-if="isCreate" v-if="isCreate"
@@ -605,13 +567,9 @@ export default {
:label="t('harvester.virtualMachine.detail.tabs.basics')" :label="t('harvester.virtualMachine.detail.tabs.basics')"
> >
<CpuMemory <CpuMemory
class="mb-20"
:cpu="cpu" :cpu="cpu"
:max-cpu="maxCpu"
:memory="memory" :memory="memory"
:max-memory="maxMemory"
:mode="mode" :mode="mode"
:enable-hot-plug="cpuMemoryHotplugEnabled"
@updateCpuMemory="updateCpuMemory" @updateCpuMemory="updateCpuMemory"
/> />
@@ -713,7 +671,7 @@ export default {
</Tab> </Tab>
<Tab <Tab
v-if="enabledPCI && usbPassthroughEnabled" v-if="enabledPCI"
:label="t('harvester.tab.usbDevices')" :label="t('harvester.tab.usbDevices')"
name="usbDevices" name="usbDevices"
:weight="-7" :weight="-7"
@@ -739,35 +697,22 @@ export default {
/> />
</Tab> </Tab>
<Tab
name="labels"
:label="t('generic.labels')"
:weight="-9"
>
<Banner color="info">
<t k="harvester.virtualMachine.labels.banner" />
</Banner>
<KeyValue
key="labels"
:value="value.labels"
:add-label="t('labels.addLabel')"
:mode="mode"
:read-allowed="false"
:value-can-be-empty="true"
@update:value="value.setLabels($event)"
/>
</Tab>
<Tab <Tab
name="instanceLabel" name="instanceLabel"
:label="t('harvester.tab.instanceLabel')" :label="t('harvester.tab.instanceLabel')"
:weight="-10" :weight="-8"
> >
<Banner color="info"> <Labels
<t k="harvester.virtualMachine.instanceLabels.banner" /> :default-container-class="'labels-and-annotations-container'"
</Banner> :value="value"
:mode="mode"
:display-side-by-side="false"
:show-annotations="false"
:show-label-title="false"
>
<template #labels="{toggler}">
<KeyValue <KeyValue
key="instance-labels" key="labels"
:value="value.instanceLabels" :value="value.instanceLabels"
:protected-keys="value.systemLabels || []" :protected-keys="value.systemLabels || []"
:toggle-filter="toggler" :toggle-filter="toggler"
@@ -777,33 +722,14 @@ export default {
:value-can-be-empty="true" :value-can-be-empty="true"
@update:value="value.setInstanceLabels($event)" @update:value="value.setInstanceLabels($event)"
/> />
</Tab> </template>
</Labels>
<Tab
name="annotations"
:label="t('harvester.tab.annotations')"
:weight="-11"
>
<Banner color="info">
<t k="harvester.virtualMachine.annotations.banner" />
</Banner>
<KeyValue
key="annotations"
:value="value.annotations"
:protected-keys="value.systemAnnotations || []"
:toggle-filter="toggler"
:add-label="t('labels.addAnnotation')"
:mode="mode"
:read-allowed="false"
:value-can-be-empty="true"
@update:value="value.setAnnotations($event)"
/>
</Tab> </Tab>
<Tab <Tab
name="advanced" name="advanced"
:label="t('harvester.tab.advanced')" :label="t('harvester.tab.advanced')"
:weight="-12" :weight="-9"
> >
<div class="row mb-20"> <div class="row mb-20">
<div class="col span-6"> <div class="col span-6">
@@ -901,7 +827,6 @@ export default {
ref="yamlEditor" ref="yamlEditor"
:user-script="userScript" :user-script="userScript"
:mode="mode" :mode="mode"
:os-type="osType"
:view-code="isWindows" :view-code="isWindows"
:namespace="value.metadata.namespace" :namespace="value.metadata.namespace"
:network-script="networkScript" :network-script="networkScript"
@@ -948,29 +873,11 @@ export default {
:mode="mode" :mode="mode"
/> />
<Checkbox
v-if="value.tpmPersistentStateFeatureEnabled && tpmEnabled"
v-model:value="tpmPersistentStateEnabled"
class="check"
type="checkbox"
:label="t('harvester.virtualMachine.advancedOptions.tpmPersistentState')"
:mode="mode"
/>
<Checkbox <Checkbox
v-model:value="efiEnabled" v-model:value="efiEnabled"
class="check" class="check"
type="checkbox" type="checkbox"
:label="t('harvester.virtualMachine.advancedOptions.efiEnabled')" :label="t('harvester.virtualMachine.efiEnabled')"
:mode="mode"
/>
<Checkbox
v-if="value.efiPersistentStateFeatureEnabled && efiEnabled"
v-model:value="efiPersistentStateEnabled"
class="check"
type="checkbox"
:label="t('harvester.virtualMachine.advancedOptions.efiPersistentState')"
:mode="mode" :mode="mode"
/> />
@@ -979,10 +886,9 @@ export default {
v-model:value="secureBoot" v-model:value="secureBoot"
class="check" class="check"
type="checkbox" type="checkbox"
:label="t('harvester.virtualMachine.advancedOptions.secureBoot')" :label="t('harvester.virtualMachine.secureBoot')"
:mode="mode" :mode="mode"
/> />
<Banner <Banner
v-if="showCpuPinningBanner" v-if="showCpuPinningBanner"
color="warning" color="warning"
@@ -81,7 +81,6 @@ export default {
:apply-hooks="applyHooks" :apply-hooks="applyHooks"
:yaml-modifiers="yamlModifiers" :yaml-modifiers="yamlModifiers"
@finish="save" @finish="save"
@error="e=>errors=e"
> >
<NameNsDescription <NameNsDescription
:value="value" :value="value"
@@ -235,7 +235,6 @@ export default {
:apply-hooks="applyHooks" :apply-hooks="applyHooks"
:yaml-modifiers="yamlModifiers" :yaml-modifiers="yamlModifiers"
@finish="save" @finish="save"
@error="e=>errors=e"
> >
<NameNsDescription <NameNsDescription
:value="value" :value="value"
@@ -67,7 +67,6 @@ export default {
:mode="mode" :mode="mode"
:errors="errors" :errors="errors"
@finish="save" @finish="save"
@error="e=>errors=e"
> >
<NameNsDescription <NameNsDescription
:value="value" :value="value"
@@ -373,7 +373,6 @@ export default {
:mode="mode" :mode="mode"
:errors="errors" :errors="errors"
@finish="save" @finish="save"
@error="e=>errors=e"
> >
<NameNsDescription <NameNsDescription
:value="value" :value="value"
@@ -1,5 +1,4 @@
<script> <script>
import { NORMAN } from '@shell/config/types';
import { HCI } from '../types'; import { HCI } from '../types';
export default { export default {
@@ -14,24 +13,10 @@ export default {
const harvesterSettings = await this.$store.dispatch('harvester/findAll', { type: HCI.SETTING }); const harvesterSettings = await this.$store.dispatch('harvester/findAll', { type: HCI.SETTING });
this.harvesterSettings = harvesterSettings; this.harvesterSettings = harvesterSettings;
const clusterRoleTemplateBindingSchema = this.$store.getters['rancher/schemaFor'](NORMAN.CLUSTER_ROLE_TEMPLATE_BINDING);
if (clusterRoleTemplateBindingSchema) {
const normanBindings = await this.$store.dispatch('rancher/findAll', { type: NORMAN.CLUSTER_ROLE_TEMPLATE_BINDING }, { root: true });
this.clusterRoleTemplateBinding = normanBindings;
}
}, },
data() { data() {
const user = this.$store.getters['auth/v3User']; return { harvesterSettings: [] };
return {
harvesterSettings: [],
clusterRoleTemplateBinding: [],
user,
};
}, },
computed: { computed: {
@@ -46,28 +31,19 @@ export default {
} catch (e) {} } catch (e) {}
return isMatch; return isMatch;
},
isClusterOwner() {
const template = this.clusterRoleTemplateBinding.find((template) => {
return template.userId === this.user?.id;
});
return template?.roleTemplateId === 'cluster-owner';
},
shouldShowError() {
return this.isClusterOwner && !this.isMatch && this.value;
} }
} }
}; };
</script> </script>
<template> <template>
<div> <div v-if="isMatch">
{{ value }}
</div>
<div v-else>
{{ value }} {{ value }}
<p <p
v-if="shouldShowError" v-if="value"
class="text-error" class="text-error"
> >
{{ t('harvester.backup.matchTarget') }} {{ t('harvester.backup.matchTarget') }}
@@ -35,26 +35,11 @@ export default {
}, },
computed: { computed: {
// Return VM instance IP and VM annotation IP
ips() { ips() {
if (this.vmiIp.length) {
return [...this.vmiIp, ...this.networkAnnotationIP] return [...this.vmiIp, ...this.networkAnnotationIP]
.filter(Boolean) .filter(Boolean)
.sort((a, b) => a.ip < b.ip ? -1 : 1); .sort((a, b) => a.ip < b.ip ? -1 : 1);
}
return this.customAnnotationIP;
},
customAnnotationIP() {
const annotationIp = get(this.row, `metadata.annotations."${ HCI_ANNOTATIONS.CUSTOM_IP }"`);
if (annotationIp && isIpv4(annotationIp)) {
return [{
name: 'custom-ip', ip: annotationIp, isCustom: true
}];
}
return [];
}, },
networkAnnotationIP() { networkAnnotationIP() {
@@ -112,13 +97,12 @@ export default {
<template> <template>
<div v-if="showIP"> <div v-if="showIP">
<span <span
v-for="{ ip, name, isCustom } in ips" v-for="{ip, name} in ips"
:key="`${ip}-${name}`" :key="ip"
> >
<CopyToClipboardText <CopyToClipboardText
v-clean-tooltip="isCustom ? t('harvester.formatters.harvesterIpAddress.customIpTooltip') : name" v-clean-tooltip="name"
:text="ip" :text="ip"
:plain="isCustom"
/> />
</span> </span>
</div> </div>
@@ -2,7 +2,7 @@
import ConsumptionGauge from '@shell/components/ConsumptionGauge'; import ConsumptionGauge from '@shell/components/ConsumptionGauge';
import { METRIC, NODE } from '@shell/config/types'; import { METRIC, NODE } from '@shell/config/types';
import { formatSi, exponentNeeded, UNITS, parseSi } from '@shell/utils/units'; import { formatSi, exponentNeeded, UNITS, parseSi } from '@shell/utils/units';
import { UNIT_SUFFIX } from '../utils/unit';
export default { export default {
name: 'HarvesterMemoryUsed', name: 'HarvesterMemoryUsed',
components: { ConsumptionGauge }, components: { ConsumptionGauge },
@@ -51,7 +51,7 @@ export default {
memoryUnits() { memoryUnits() {
const exponent = exponentNeeded(this.memoryTotal, 1024); const exponent = exponentNeeded(this.memoryTotal, 1024);
return `${ UNITS[exponent] }${ UNIT_SUFFIX }`; return `${ UNITS[exponent] }iB`;
}, },
node() { node() {
@@ -2,7 +2,6 @@
import ConsumptionGauge from '@shell/components/ConsumptionGauge'; import ConsumptionGauge from '@shell/components/ConsumptionGauge';
import { LONGHORN } from '@shell/config/types'; import { LONGHORN } from '@shell/config/types';
import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units'; import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
import { UNIT_SUFFIX } from '../utils/unit';
export default { export default {
name: 'HarvesterStorageUsed', name: 'HarvesterStorageUsed',
@@ -76,13 +75,13 @@ export default {
allocatedUnits() { allocatedUnits() {
const exponent = exponentNeeded(this.storageStats.total, 1024); const exponent = exponentNeeded(this.storageStats.total, 1024);
return `${ UNITS[exponent] }${ UNIT_SUFFIX }`; return `${ UNITS[exponent] }iB`;
}, },
usedUnits() { usedUnits() {
const exponent = exponentNeeded(this.storageStats.maximum, 1024); const exponent = exponentNeeded(this.storageStats.maximum, 1024);
return `${ UNITS[exponent] }${ UNIT_SUFFIX }`; return `${ UNITS[exponent] }iB`;
}, },
formatUsed() { formatUsed() {
+11 -213
View File
@@ -38,30 +38,6 @@ asyncButton:
success: Restarted success: Restarted
waiting: Restarting&hellip; waiting: Restarting&hellip;
dialog:
confirmExecution:
title: Are you sure?
andOthers: |-
{count, plural,
=0 {}
=1 { and <b>one other </b>}
other { and <b>{count} other </b>}
}
protip: "Tip: Hold the {alternateLabel} key while clicking action to bypass this confirmation"
stop:
message: "Are you sure you want to stop the {type} {names}?"
pause:
message: "Are you sure you want to pause the {type} {names}?"
restart:
message: "Are you sure you want to restart the {type} {names}?"
softreboot:
message: "Are you sure you want to soft reboot the {type} {names}?"
promptRemove:
title: "Delete {type}"
warningMessage: "Deleting the selected {type} permanently removes all resources on {thisOrThese} {type}. This action is irreversible. Do you want to continue?"
confirmName: "Type <b>{nameToMatch}</b> to delete the {type}:"
protip: "Tip: Hold the {alternateLabel} key while clicking Delete to bypass the confirmation step."
harvester: harvester:
branding: branding:
logos: logos:
@@ -108,14 +84,6 @@ harvester:
tip: Please enter a virtual machine name! tip: Please enter a virtual machine name!
success: 'Virtual machine { name } cloned successfully.' success: 'Virtual machine { name } cloned successfully.'
failed: 'Failed clone virtual machine!' failed: 'Failed clone virtual machine!'
osImage:
title: Delete Image
message: Are you sure you want to delete the image { name } ?
downloadImage:
title: Download Image
banner: 'This action takes a while depending on the image size ({ size }). Please be patient.'
startMessage : 'The download process will auto start once the conversion is complete.'
download: Download
exportImage: exportImage:
title: Export to Image title: Export to Image
name: Name name: Name
@@ -136,31 +104,13 @@ harvester:
delete: Delete delete: Delete
bundle: bundle:
title: Generate a Support Bundle title: Generate a Support Bundle
titleDescription: Collect system-related logs in Harvester to assist with troubleshooting and support.
tip: Each field below specifies an aspect of the support bundle. For detailed explanations, please refer to the <a href="{doc}" target="_blank">documentation</a>.
url: Issue URL url: Issue URL
description: Description description: Description
namespaces: requiredDesc: Description is required!
label: Namespaces titleDescription: Collect system-related logs in Harvester to help with troubleshooting and support.
tooltip: Select additional namespaces to include in the support bundle.<br/>See docs support-bundle-namespaces for detail.
selectAll: 'Select All'
unselectAll: 'Unselect All'
timeout:
label: Timeout
tooltip: Minutes allows for completion of the support bundle generation process.<br/>See docs support-bundle-timeout for detail.
expiration:
label: Expiration
tooltip: Minutes before deleting packaged but not downloaded support bundle.<br/>See docs support-bundle-expiration for detail.
nodeTimeout:
label: Node Collection Timeout
tooltip: Minutes allowed for collecting logs/configurations on nodes.<br/>See docs support-bundle-node-collection-timeout for detail.
hotplug: hotplug:
success: 'Volume { diskName } is mounted to the virtual machine { vm }.' success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
title: Add Volume title: Add Volume
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: hotunplug:
success: 'Volume { name } is detached successfully.' success: 'Volume { name } is detached successfully.'
snapshot: snapshot:
@@ -217,7 +167,6 @@ harvester:
suspendSchedule: Suspend suspendSchedule: Suspend
restoreExistingVM: Replace Existing restoreExistingVM: Replace Existing
migrate: Migrate migrate: Migrate
cpuAndMemoryHotplug: Edit CPU and Memory
abortMigration: Abort Migration abortMigration: Abort Migration
createTemplate: Generate Template createTemplate: Generate Template
enableMaintenance: Enable Maintenance Mode enableMaintenance: Enable Maintenance Mode
@@ -251,8 +200,6 @@ harvester:
{count, plural, {count, plural,
=1 {core} =1 {core}
other {cores}} other {cores}}
harvesterIpAddress:
customIpTooltip: "Custom IP (set via annotation)"
tableHeaders: tableHeaders:
imageEncryption: Encryption imageEncryption: Encryption
size: Size size: Size
@@ -300,7 +247,6 @@ harvester:
quotas: Quotas quotas: Quotas
snapshots: Snapshots snapshots: Snapshots
instanceLabel: Instance Labels instanceLabel: Instance Labels
annotations: Annotations
fields: fields:
version: Version version: Version
name: Name name: Name
@@ -401,8 +347,6 @@ harvester:
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.' 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.'
hash: hash:
sha512: 'Invalid SHA512 checksum.' sha512: 'Invalid SHA512 checksum.'
subnet:
excludeIps: 'Exclude IPs cannot be empty. Please remove or fill in the exclude IPs.'
dashboard: dashboard:
label: Dashboard label: Dashboard
@@ -576,10 +520,6 @@ harvester:
virtualMachine: virtualMachine:
label: Virtual Machines label: Virtual Machines
osType: OS Type osType: OS Type
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
instance: instance:
singleInstance: singleInstance:
multipleInstance: multipleInstance:
@@ -644,17 +584,15 @@ harvester:
enableUsb: Enable USB Tablet enableUsb: Enable USB Tablet
advancedOptions: advancedOptions:
tpm: Enable TPM tpm: Enable TPM
tpmPersistentState: TPM Persistent State
cpuManager: cpuManager:
prefix: You must enable CPU Manager for at least one node in prefix: You must enable CPU Manager for at least one node in
middle: 'host page' middle: 'host page'
suffix: to enable CPU Pinning for VM suffix: to enable CPU Pinning for VM
efiEnabled: Booting in EFI mode
efiPersistentState: EFI Persistent State
secureBoot: Secure Boot
usbTip: Provides an absolute pointer device which often helps with getting a consistent mouse cursor position in VNC. usbTip: Provides an absolute pointer device which often helps with getting a consistent mouse cursor position in VNC.
sshTitle: Add Public SSH Key sshTitle: Add Public SSH Key
imageTip: An external URL to the .iso, .img, .qcow2 or .raw that the virtual machine should be created from. imageTip: An external URL to the .iso, .img, .qcow2 or .raw that the virtual machine should be created from.
efiEnabled: Booting in EFI mode
secureBoot: Secure Boot
volume: volume:
dragTip: Drag and drop volumes, or use the volume's arrows, to change the boot order. dragTip: Drag and drop volumes, or use the volume's arrows, to change the boot order.
volumeTip: The virtual machine only contains a CD-ROM volume. You may want to add additional disk volumes. volumeTip: The virtual machine only contains a CD-ROM volume. You may want to add additional disk volumes.
@@ -675,7 +613,6 @@ harvester:
setFirst: Set as root volume setFirst: Set as root volume
saveVolume: Update Volume saveVolume: Update Volume
encryption: Encryption encryption: Encryption
vmImageVolumeTip: Disk size ({diskSize}) should greater than selected image virtual size ({imageVirtualSize})
lockTooltip: lockTooltip:
all: All volumes are encrypted. all: All volumes are encrypted.
partial: Some volumes are encrypted. partial: Some volumes are encrypted.
@@ -705,7 +642,7 @@ harvester:
network: network:
label: Network Data Template label: Network Data Template
title: "Network Data:" title: "Network Data:"
tip: "The network-data configuration allows you to customize the instance's networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>" tip: "The network-data configuration allows you to customize the instance’s networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>"
scheduling: scheduling:
affinity: affinity:
anyNode: 'Run virtual machine on any available node' anyNode: 'Run virtual machine on any available node'
@@ -725,9 +662,6 @@ harvester:
invalidUser: Invalid Username. invalidUser: Invalid Username.
input: input:
name: Name name: Name
cpu: CPU
maxCpu: Maximum CPU
maxMemory: Maximum Memory
memory: Memory memory: Memory
image: Image image: Image
sshKey: SSHKey sshKey: SSHKey
@@ -788,7 +722,6 @@ harvester:
dedicatedResources: Dedicated Resources dedicatedResources: Dedicated Resources
down: Virtual machine not running down: Virtual machine not running
affinityRules: Affinity Rules affinityRules: Affinity Rules
phase: Phase
sourceNode: Source Node sourceNode: Source Node
targetNode: Target Node targetNode: Target Node
started: Started started: Started
@@ -822,12 +755,6 @@ harvester:
inNamespaces: "Workloads in these namespaces" inNamespaces: "Workloads in these namespaces"
namespaces: namespaces:
label: Namespaces label: Namespaces
instanceLabels:
banner: These labels are automatically synchronized to the virtual machine instance.
labels:
banner: These key values are added as labels to the virtual machine.
annotations:
banner: These key values are added as annotations to the virtual machine.
volume: volume:
label: Volumes label: Volumes
@@ -837,7 +764,6 @@ harvester:
datasource: Data Source datasource: Data Source
details: Details details: Details
size: Size size: Size
volumeMode: Volume Mode
source: Source source: Source
kind: Kind kind: Kind
sourceOptions: sourceOptions:
@@ -888,12 +814,8 @@ harvester:
=1 {1 image is uploading, please do not refresh or close the page.} =1 {1 image is uploading, please do not refresh or close the page.}
other {{count} images are uploading, please do not refresh or close the page.} other {{count} images are uploading, please do not refresh or close the page.}
} }
osUpgrade:
uploading: "{name} is uploading, please do not refresh or close the page."
checksum: Checksum checksum: Checksum
checksumTip: Validate the image using the SHA512 checksum, if specified. checksumTip: Validate the image using the SHA512 checksum, if specified.
tooltip:
imported: Created automatically by the vm-import-controller
vmTemplate: vmTemplate:
label: Templates label: Templates
@@ -916,22 +838,17 @@ harvester:
currentVersion: CURRENT VERSION currentVersion: CURRENT VERSION
versionLabel: AVAILABLE COMPATIBLE VERSIONS versionLabel: AVAILABLE COMPATIBLE VERSIONS
enableLogging: Enable Logging enableLogging: Enable Logging
skipSingleReplicaDetachedVol: Skip checking single-replica detached volumes
upgradeNode: Upgrading Node upgradeNode: Upgrading Node
upgradeSysService: Upgrading System Service upgradeSysService: Upgrading System Service
upgradeImage: Download Upgrade Image upgradeImage: Download Upgrade Image
osUpgrade: Cluster Upgrade osUpgrade: OS Upgrade
uploadNew: Upload New Image uploadNew: Upload New Image
deleteHeader: Please select an image to delete.
deleteExisting: Delete Existing Image
selectExisting: Select Existing Image selectExisting: Select Existing Image
createRepository: Creating Upgrade Repository createRepository: Creating Upgrade Repository
succeeded: Succeeded succeeded: Succeeded
releaseTip: Please read the upgrade documentation carefully. You can view details on the <a href="{url}" target="_blank">Harvester Release Notes</a>. releaseTip: Please read the upgrade documentation carefully. You can view details on the <a href="{url}" target="_blank">Harvester Release Notes</a>.
checkReady: I have read and understood the upgrade instructions related to this Harvester version. checkReady: I have read and understood the upgrade instructions related to this Harvester version.
pending: Pending pending: Pending
upload:
duplicatedFile: The file you are trying to upload already exists.
repoInfo: repoInfo:
upgradeStatus: Upgrade Status upgradeStatus: Upgrade Status
os: OS os: OS
@@ -1029,57 +946,6 @@ harvester:
progress: Restore in progress progress: Restore in progress
complete: Restore completed complete: Restore completed
subnet:
cidrBlock:
tooltip: The subnet range in CIDR notation. Note that the CIDR blocks of different Subnets' within the same VPC cannot overlap.
label: CIDR Block
placeholder: e.g. 172.20.0.0/16
protocol:
label: Protocol
provider:
tooltip: Network provider for this Subnet. Must be one of the Virtual Machine Networks in OverlayNetwork type.
label: Provider
vpc:
label: Virtual Private Cloud
gateway:
label: Gateway IP
placeholder: e.g. 172.20.0.1
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.
allowSubnet:
label: Allow Subnets
tooltip: You can specify certain subnets which can communicate with this subnet.
addSubnet: Add Allow Subnet
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.
vpc:
noAddonEnabled:
prefix: The kubeovn-operator add-on is not enabled, click
middle: here
suffix: to enable the add-on to successfully create VPC and subnet.
label: Virtual Private Cloud
noChild: There is no subnet defined in this Virtual Private Cloud.
createSubnet: Create Subnet
staticRoutes:
label: Static Routes
cidr:
label: CIDR
placeholder: e.g. 172.16.0.0/16
nextHopIP:
tooltip: The localConnectIP on the other end of the peering VPC.
label: Next Hop IP
placeholder: e.g. 169.254.0.2
vpcPeerings:
label: VPC Peerings
localConnectIP:
tooltip: The designated IP address in CIDR notation of the VPC peering endpoint. Note that both IP addresses (for the VPC and the other VPC) should belong to the same subnet range and should not conflict with the cidrBlock in any existing subnets.
label: Local Connect IP
placeholder: e.g. 169.254.0.1/16
remoteVpc:
label: Remote VPC
network: network:
label: Virtual Machine Networks label: Virtual Machine Networks
tabs: tabs:
@@ -1122,7 +988,6 @@ harvester:
accessKeyId: Specify your access key ID accessKeyId: Specify your access key ID
secretAccessKey: Specify your secret access key secretAccessKey: Specify your secret access key
cert: Upload a self-signed SSL certificate cert: Upload a self-signed SSL certificate
noSearchMatch: No settings match your search.
vlanChangeTip: The newly modified default network interface only applies to newly added nodes, not existing ones. vlanChangeTip: The newly modified default network interface only applies to newly added nodes, not existing ones.
defaultPhysicalNIC: Default Network Interface defaultPhysicalNIC: Default Network Interface
modifiedMessage: Settings that have been customized from default settings are tagged with 'Modified'. modifiedMessage: Settings that have been customized from default settings are tagged with 'Modified'.
@@ -1136,9 +1001,6 @@ harvester:
publicCertificate: Public Certificate publicCertificate: Public Certificate
privateKey: Private Key privateKey: Private Key
ca: CA ca: CA
rancherCluster:
kubeConfig: Rancher KubeConfig
removeUpstreamClusterWhenNamespaceIsDeleted: Remove Upstream Cluster When Namespace Is Deleted
storageNetwork: storageNetwork:
range: range:
placeholder: e.g. 172.16.0.0/24 placeholder: e.g. 172.16.0.0/24
@@ -1147,28 +1009,14 @@ harvester:
clusterNetwork: Cluster Network clusterNetwork: Cluster Network
vlan: VLAN ID vlan: VLAN ID
exclude: exclude:
label: Exclude IPs label: Exclude
placeholder: e.g. 172.16.0.1 placeholder: e.g. 172.16.0.1/32
invalid: '"Exclude list" is invalid.' invalid: '"Exclude list" is invalid.'
addIp: Add Exclude IP addIp: Add Exclude IP
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.' warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
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>.' tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 4 + 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>.'
vmForceDeletionPolicy: vmForceDeletionPolicy:
period: Period period: Period
vmMigrationNetwork:
parseError: "Failed to parse existing configuration."
fetchError: "Failed to load required network resources: {error}. Please refresh the page or try again later."
clusterNetwork: Cluster Network
vlan: VLAN ID
range:
placeholder: e.g. 172.16.0.0/24
label: IP Range
invalid: '"Range" is invalid.'
exclude:
label: Excluded IPs
placeholder: e.g. 172.16.0.1/32
invalid: '"Exclude list" is invalid.'
addButton: Add Exclude IP
ratio : Ratio ratio : Ratio
autoRotateRKE2Certs: autoRotateRKE2Certs:
expiringInHours: Expiring in expiringInHours: Expiring in
@@ -1182,11 +1030,6 @@ harvester:
provisioner: Provisioner provisioner: Provisioner
volumeSnapshotClassName: Volume Snapshot Class Name volumeSnapshotClassName: Volume Snapshot Class Name
backupVolumeSnapshotClassName: Backup Volume Snapshot Class Name backupVolumeSnapshotClassName: Backup Volume Snapshot Class Name
csiOnlineExpandValidation:
provisioner: Provisioner
value: Value
invalidJsonFormat: "Configuration value is not a valid JSON format: {error}"
failedToLoadDrivers: "Failed to load CSI drivers. Error: {error}"
containerdRegistry: containerdRegistry:
mirrors: mirrors:
mirrors: Mirrors mirrors: Mirrors
@@ -1208,19 +1051,10 @@ harvester:
addConfig: Add Configuration addConfig: Add Configuration
upgrade: upgrade:
unknownImageName: Image name is not found. selectExitImage: Please select the OS image to upgrade.
imageUrl: Please input a valid image URL. imageUrl: Please input a valid image URL.
chooseFile: Please select to upload an image. chooseFile: Please select to upload an image.
checksum: Checksum checksum: Checksum
networkError: Unable to upload the image. Resolve network issues that may have occurred and try again.
cancelUpload: Cancelled the image upload.
uploadSuccess: "{name} uploaded successfully. Press Upgrade button to start the cluster upgrade process."
deleteImage: Please select an image to delete.
deleteSuccess: "{name} deleted successfully."
imagePreloadStrategy: Image Preload Strategy
restoreVM: Restore VM
strategyType: Strategy Type
concurrency: Concurrency
harvesterMonitoring: harvesterMonitoring:
label: Harvester Monitoring label: Harvester Monitoring
section: section:
@@ -1382,32 +1216,6 @@ harvester:
allowedTopologies: allowedTopologies:
title: Allowed Topologies title: Allowed Topologies
tooltip: Allowed Topologies helps scheduling virtual machines on hosts which match all of below expressions. tooltip: Allowed Topologies helps scheduling virtual machines on hosts which match all of below expressions.
internal:
label: Internal Storage Class
cannotDeleteTooltip: Internal storage class volumes cannot be deleted
cannotDeleteOrDefaultTooltip: Internal storage classes cannot be deleted or set as default
cdiSettings:
label: CDI Settings
volumeModeAccessModes:
label: Volume Mode / Access Modes
tooltip: Specifies the default volume mode and access modes.
add: Add
volumeMode: Volume Mode
accessModes:
label: Access Modes
volumeSnapshotClass:
label: Volume Snapshot Class
tooltip: Sets the Volume Snapshot Class name to be used when taking snapshots of PVCs under this StorageClass.
cloneStrategy:
label: Clone Strategy
tooltip: Defines the clone strategy to use for volumes created with this StorageClass.
copy: Copy
snapshot: Snapshot
csiClone: CSI Clone
fileSystemOverhead:
label: File System Overhead
tooltip: Specifies the percentage of filesystem overhead to consider when calculating PVC size.
placeholder: e.g. 0.05 (up to 3 decimal places, between 0 and 1)
vlanConfig: vlanConfig:
title: Network Configuration title: Network Configuration
@@ -1712,7 +1520,6 @@ advancedSettings:
'harv-backup-target': Custom backup target to store virtual machine backups. 'harv-backup-target': Custom backup target to store virtual machine backups.
'branding': Branding allows administrators to globally re-brand the UI by customizing the Harvester product name, logos, and color scheme. 'branding': Branding allows administrators to globally re-brand the UI by customizing the Harvester product name, logos, and color scheme.
'harv-csi-driver-config': Configure additional information for CSI drivers. 'harv-csi-driver-config': Configure additional information for CSI drivers.
'harv-csi-online-expand-validation': Allow online volume expansion for specific CSI drivers.
'harv-containerd-registry': Containerd Registry Configuration to connect private registries. 'harv-containerd-registry': Containerd Registry Configuration to connect private registries.
'harv-log-level': Configure Harvester server log level. Defaults to Info. 'harv-log-level': Configure Harvester server log level. Defaults to Info.
'harv-server-version': Harvester server version. 'harv-server-version': Harvester server version.
@@ -1731,7 +1538,7 @@ advancedSettings:
'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down. '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-ssl-parameters': Custom SSL Parameters for TLS validation.
'harv-storage-network': 'Longhorn storage-network setting.' 'harv-storage-network': 'Longhorn storage-network setting.'
'harv-support-bundle-namespaces': Select additional namespaces to include in the support bundle. 'harv-support-bundle-namespaces': Specify resources in other namespaces to be collected by the support package.
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage. '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>. '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>.
'harv-release-download-url': This setting allows you to configure the <code>upgrade release download</code> URL address. Harvester will get the ISO URL and checksum value from the (<code>$URL</code>/<code>$VERSION</code>/version.yaml) file hosted by the configured URL. 'harv-release-download-url': This setting allows you to configure the <code>upgrade release download</code> URL address. Harvester will get the ISO URL and checksum value from the (<code>$URL</code>/<code>$VERSION</code>/version.yaml) file hosted by the configured URL.
@@ -1741,10 +1548,6 @@ advancedSettings:
'harv-kubeconfig-default-token-ttl-minutes': 'TTL (in minutes) applied on Harvester administration kubeconfig files. Default is 0, which means to never expire.' 'harv-kubeconfig-default-token-ttl-minutes': 'TTL (in minutes) applied on Harvester administration kubeconfig files. Default is 0, which means to never expire.'
'harv-longhorn-v2-data-engine-enabled': 'Enable the Longhorn V2 data engine. Default is false. <ul><li>Changing this setting will restart RKE2 on all nodes. This will not affect running VM workloads.</li><li>If you see "not enough hugepages-2Mi capacity" errors when enabling this setting, wait a minute for the error to clear. If the error remains, reboot the affected node.</li></ul>' 'harv-longhorn-v2-data-engine-enabled': 'Enable the Longhorn V2 data engine. Default is false. <ul><li>Changing this setting will restart RKE2 on all nodes. This will not affect running VM workloads.</li><li>If you see "not enough hugepages-2Mi capacity" errors when enabling this setting, wait a minute for the error to clear. If the error remains, reboot the affected node.</li></ul>'
'harv-additional-guest-memory-overhead-ratio': 'The ratio for kubevirt to adjust the VM overhead memory. The value could be zero, empty value or floating number between 1.0 and 10.0, default to 1.5.' 'harv-additional-guest-memory-overhead-ratio': 'The ratio for kubevirt to adjust the VM overhead memory. The value could be zero, empty value or floating number between 1.0 and 10.0, default to 1.5.'
'harv-upgrade-config': 'Configure image preloading and VM restore options for upgrades. See related fields in <a href="{url}" target="_blank" rel="noopener">settings/upgrade-config</a>'
'harv-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.'
typeLabel: typeLabel:
kubevirt.io.virtualmachine: |- kubevirt.io.virtualmachine: |-
@@ -1802,11 +1605,6 @@ typeLabel:
one { Virtual Machine Backup } one { Virtual Machine Backup }
other { Virtual Machine Backups } other { Virtual Machine Backups }
} }
kubeovn.io.vpc: |-
{count, plural,
one { Virtual Private Cloud }
other { Virtual Private Clouds }
}
harvesterhci.io.cloudtemplate: |- harvesterhci.io.cloudtemplate: |-
{count, plural, {count, plural,
one { Cloud Configuration Template } one { Cloud Configuration Template }
@@ -8,7 +8,9 @@ import Banner from '@components/Banner/Banner.vue';
import MessageLink from '@shell/components/MessageLink'; import MessageLink from '@shell/components/MessageLink';
import SortableTable from '@shell/components/SortableTable'; import SortableTable from '@shell/components/SortableTable';
import { allHash, setPromiseResult } from '@shell/utils/promise'; import { allHash, setPromiseResult } from '@shell/utils/promise';
import { parseSi, formatSi, exponentNeeded, UNITS } from '@shell/utils/units'; import {
parseSi, formatSi, exponentNeeded, UNITS, createMemoryValues
} from '@shell/utils/units';
import { REASON } from '@shell/config/table-headers'; import { REASON } from '@shell/config/table-headers';
import { import {
EVENT, METRIC, NODE, SERVICE, PVC, LONGHORN, POD, COUNT, NETWORK_ATTACHMENT EVENT, METRIC, NODE, SERVICE, PVC, LONGHORN, POD, COUNT, NETWORK_ATTACHMENT
@@ -25,21 +27,22 @@ import { isEmpty } from '@shell/utils/object';
import { HCI } from '../types'; import { HCI } from '../types';
import HarvesterUpgrade from '../components/HarvesterUpgrade'; import HarvesterUpgrade from '../components/HarvesterUpgrade';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester'; import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
import { UNIT_SUFFIX } from '../utils/unit';
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(minMax); dayjs.extend(minMax);
const PARSE_RULES = { const PARSE_RULES = {
memory: {
format: { format: {
addSuffix: true, addSuffix: true,
firstSuffix: UNIT_SUFFIX, firstSuffix: 'B',
increment: 1024, increment: 1024,
maxExponent: 99, maxExponent: 99,
maxPrecision: 2, maxPrecision: 2,
minExponent: 0, minExponent: 0,
startingExponent: 0, startingExponent: 0,
suffix: UNIT_SUFFIX, suffix: 'iB',
}
} }
}; };
@@ -132,7 +135,6 @@ export default {
const hash = { const hash = {
vms: this.fetchClusterResources(HCI.VM), vms: this.fetchClusterResources(HCI.VM),
pvcs: this.fetchClusterResources(PVC),
nodes: this.fetchClusterResources(NODE), nodes: this.fetchClusterResources(NODE),
events: this.fetchClusterResources(EVENT), events: this.fetchClusterResources(EVENT),
metricNodes: this.fetchClusterResources(METRIC.NODE), metricNodes: this.fetchClusterResources(METRIC.NODE),
@@ -222,7 +224,6 @@ export default {
nodes: [], nodes: [],
metricNodes: [], metricNodes: [],
vms: [], vms: [],
pvcs: [],
monitoring: {}, monitoring: {},
VM_DASHBOARD_METRICS_URL, VM_DASHBOARD_METRICS_URL,
CLUSTER_METRICS_SUMMARY_URL, CLUSTER_METRICS_SUMMARY_URL,
@@ -287,14 +288,6 @@ export default {
out[resource.type].name = this.t(`typeLabel."${ resource.spoofed.name }"`, { count: out[resource.type].total }); out[resource.type].name = this.t(`typeLabel."${ resource.spoofed.name }"`, { count: out[resource.type].total });
} }
if (resource.type === PVC) {
// filter out the golden image volumes
const goldenImageVolumeCount = (this.pvcs || []).filter((pvc) => pvc.isGoldenImageVolume).length;
out[resource.type].useful = out[resource.type].useful - goldenImageVolumeCount;
out[resource.type].total = out[resource.type].total - goldenImageVolumeCount;
}
if (resource.type === HCI.BLOCK_DEVICE) { if (resource.type === HCI.BLOCK_DEVICE) {
let total = 0; let total = 0;
let errorCount = 0; let errorCount = 0;
@@ -409,13 +402,13 @@ export default {
storageUsed() { storageUsed() {
const stats = this.storageStats; const stats = this.storageStats;
return this.createDisplayValues(stats.maximum, stats.used); return this.createMemoryValues(stats.maximum, stats.used);
}, },
storageAllocated() { storageAllocated() {
const stats = this.storageStats; const stats = this.storageStats;
return this.createDisplayValues(stats.total, stats.scheduled); return this.createMemoryValues(stats.total, stats.scheduled);
}, },
vmEvents() { vmEvents() {
@@ -461,7 +454,7 @@ export default {
return total + node.memoryReserved; return total + node.memoryReserved;
}, 0); }, 0);
return this.createDisplayValues(this.memoryTotal, useful); return createMemoryValues(this.memoryTotal, useful);
}, },
availableNodes() { availableNodes() {
@@ -501,7 +494,7 @@ export default {
}, },
ramUsed() { ramUsed() {
return this.createDisplayValues(this.memoryTotal, this.metricAggregations?.memory); return createMemoryValues(this.memoryTotal, this.metricAggregations?.memory);
}, },
hasMetricNodeSchema() { hasMetricNodeSchema() {
@@ -523,12 +516,10 @@ export default {
}, },
methods: { methods: {
createDisplayValues(total, useful) { createMemoryValues(total, useful) {
const parsedTotal = parseSi((total || '0').toString()); const parsedTotal = parseSi((total || '0').toString());
const parsedUseful = parseSi((useful || '0').toString()); const parsedUseful = parseSi((useful || '0').toString());
const format = this.createFormat(parsedTotal); const format = this.createMemoryFormat(parsedTotal);
const formattedTotal = formatSi(parsedTotal, format); const formattedTotal = formatSi(parsedTotal, format);
let formattedUseful = formatSi(parsedUseful, { let formattedUseful = formatSi(parsedUseful, {
...format, ...format,
@@ -547,24 +538,24 @@ export default {
useful: Number(parsedUseful), useful: Number(parsedUseful),
formattedTotal, formattedTotal,
formattedUseful, formattedUseful,
units: this.createUnits(parsedTotal), units: this.createMemoryUnits(parsedTotal),
}; };
}, },
createFormat(n) { createMemoryFormat(n) {
const exponent = exponentNeeded(n, PARSE_RULES.format.increment); const exponent = exponentNeeded(n, PARSE_RULES.memory.format.increment);
return { return {
...PARSE_RULES.format, ...PARSE_RULES.memory.format,
maxExponent: exponent, maxExponent: exponent,
minExponent: exponent, minExponent: exponent,
}; };
}, },
createUnits(n) { createMemoryUnits(n) {
const exponent = exponentNeeded(n, PARSE_RULES.format.increment); const exponent = exponentNeeded(n, PARSE_RULES.memory.format.increment);
return `${ UNITS[exponent] }${ PARSE_RULES.format.suffix }`; return `${ UNITS[exponent] }${ PARSE_RULES.memory.format.suffix }`;
}, },
async fetchClusterResources(type, opt = {}, store) { async fetchClusterResources(type, opt = {}, store) {
+2 -2
View File
@@ -101,14 +101,14 @@ export default {
const metricCol = [ const metricCol = [
{ {
name: 'cpu', name: 'cpu',
labelKey: 'harvester.dashboard.hardwareResourceGauge.cpu', labelKey: 'node.detail.glance.consumptionGauge.cpu',
value: 'id', value: 'id',
formatter: 'HarvesterCPUUsed', formatter: 'HarvesterCPUUsed',
formatterOpts: { showUsed: true }, formatterOpts: { showUsed: true },
}, },
{ {
name: 'memory', name: 'memory',
labelKey: 'harvester.dashboard.hardwareResourceGauge.memory', labelKey: 'node.detail.glance.consumptionGauge.memory',
value: 'id', value: 'id',
formatter: 'HarvesterMemoryUsed', formatter: 'HarvesterMemoryUsed',
formatterOpts: { showUsed: true }, formatterOpts: { showUsed: true },

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