mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-03 20:56:06 +08:00
Compare commits
178
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95f35c633c | ||
|
|
981287af0a | ||
|
|
94be1c1c09 | ||
|
|
09469c67e2 | ||
|
|
b12d2b5df2 | ||
|
|
887a0d0517 | ||
|
|
73e91d19a4 | ||
|
|
bee30483ad | ||
|
|
71dad21796 | ||
|
|
5704240ac5 | ||
|
|
085d216a30 | ||
|
|
ff692affb0 | ||
|
|
cfb3392e2e | ||
|
|
fe42708361 | ||
|
|
62df6f5cbb | ||
|
|
2da7d0eb58 | ||
|
|
251d92afba | ||
|
|
7f3865343d | ||
|
|
8f7a153665 | ||
|
|
2a376933f2 | ||
|
|
e5c253f044 | ||
|
|
f0a9fe76a1 | ||
|
|
a0a797d5b9 | ||
|
|
1cc6f34d84 | ||
|
|
ebfc4e8369 | ||
|
|
bd647738ae | ||
|
|
c6fb969d7e | ||
|
|
2afb04947d | ||
|
|
730c68bf14 | ||
|
|
20bee39a6c | ||
|
|
dbb199d7bb | ||
|
|
5488979448 | ||
|
|
9e588e90c2 | ||
|
|
9378277102 | ||
|
|
b5e78018a5 | ||
|
|
f411a0c0af | ||
|
|
cfa58985cf | ||
|
|
66a8f9d0e7 | ||
|
|
bf61c7dd7d | ||
|
|
56d97260c4 | ||
|
|
beabb34920 | ||
|
|
d2609157bd | ||
|
|
8fbe1943d8 | ||
|
|
ec6bc4d639 | ||
|
|
3824a14730 | ||
|
|
0fc8bece02 | ||
|
|
39764af627 | ||
|
|
bdc87bda0e | ||
|
|
e0dc77624b | ||
|
|
c3ba10bd22 | ||
|
|
5b7d54d0a3 | ||
|
|
99a216dfa0 | ||
|
|
e4c85f510e | ||
|
|
f391f018de | ||
|
|
4b2e92ea15 | ||
|
|
2f956d5946 | ||
|
|
5f8d556ea2 | ||
|
|
396ab48f1c | ||
|
|
6b8c079018 | ||
|
|
7f638e86c8 | ||
|
|
0c4955a766 | ||
|
|
3f4ff30275 | ||
|
|
8e0332a364 | ||
|
|
6700b2055e | ||
|
|
e486852f7a | ||
|
|
c19341bec9 | ||
|
|
8cbb9d6b18 | ||
|
|
00f0953592 | ||
|
|
58507f0b2e | ||
|
|
7785d7f469 | ||
|
|
2c043e0a8e | ||
|
|
6f90cae482 | ||
|
|
b4980a51e7 | ||
|
|
756ed383ac | ||
|
|
7e0a9dcd80 | ||
|
|
5fae6c3087 | ||
|
|
a994d9861e | ||
|
|
5d996c42dc | ||
|
|
88cd302ce4 | ||
|
|
e6dd8d6771 | ||
|
|
1a3822881e | ||
|
|
ee8bb21a10 | ||
|
|
e3d30a0eec | ||
|
|
532b6c4d50 | ||
|
|
f22644e058 | ||
|
|
cd128d0444 | ||
|
|
0278f51260 | ||
|
|
98efd63110 | ||
|
|
78e78ac8dd | ||
|
|
2aaa0a55a2 | ||
|
|
5c2a23924d | ||
|
|
7af8a82838 | ||
|
|
3b343bcaca | ||
|
|
6ddd35d661 | ||
|
|
bd28ba6f71 | ||
|
|
4bb67153ce | ||
|
|
7dbd442519 | ||
|
|
5a0d7f283d | ||
|
|
9fdbe9c58f | ||
|
|
f652ed9d4b | ||
|
|
21a1cd4e89 | ||
|
|
facc74ca51 | ||
|
|
74c12a0d1b | ||
|
|
3277ab4a2b | ||
|
|
4aabf0b7a3 | ||
|
|
4e8cb31e9d | ||
|
|
1b214b2b6f | ||
|
|
748c88866a | ||
|
|
c3e5c2161e | ||
|
|
f932afee68 | ||
|
|
0bbdf3bf17 | ||
|
|
bdf3ac2803 | ||
|
|
0d183a8174 | ||
|
|
8f72c33f4b | ||
|
|
22c99211f1 | ||
|
|
775330d829 | ||
|
|
18be022a9f | ||
|
|
56b4b46b5a | ||
|
|
db398ecad3 | ||
|
|
d175e3b11a | ||
|
|
905db4b12c | ||
|
|
fc31b4bb9d | ||
|
|
db29a7c31b | ||
|
|
2b3541164d | ||
|
|
ea5e9aa1f4 | ||
|
|
18a5608e72 | ||
|
|
18599fc94c | ||
|
|
e155d46483 | ||
|
|
795e5d178f | ||
|
|
2e03ac3cf7 | ||
|
|
76605fdc07 | ||
|
|
c289b001db | ||
|
|
e8d63da1eb | ||
|
|
4a456ac07d | ||
|
|
57dfddd593 | ||
|
|
66eac96575 | ||
|
|
182d92d80b | ||
|
|
1b362f570b | ||
|
|
cf798048f8 | ||
|
|
93c8399935 | ||
|
|
b72f523ddf | ||
|
|
e5ffb08df3 | ||
|
|
98a6322c11 | ||
|
|
be7e4bd80b | ||
|
|
ed2bc3100b | ||
|
|
b6ffb3e9f1 | ||
|
|
3d8a6bba7e | ||
|
|
be9311dc0c | ||
|
|
e294f4c00f | ||
|
|
a9fa928912 | ||
|
|
ef2b4d1589 | ||
|
|
a73e9f0ac1 | ||
|
|
11b3bf4c1f | ||
|
|
b775ce5f50 | ||
|
|
193d4536e3 | ||
|
|
9ca9fdb521 | ||
|
|
bcabefe9f3 | ||
|
|
c541f81dc3 | ||
|
|
4486f71c8f | ||
|
|
060105ead3 | ||
|
|
3ea73978ee | ||
|
|
f4e363396d | ||
|
|
fa16e24983 | ||
|
|
ce63ee6a19 | ||
|
|
ee28697161 | ||
|
|
85a06feb91 | ||
|
|
17dd46cee9 | ||
|
|
0ef4ff65cc | ||
|
|
cebb302730 | ||
|
|
f12717a8f4 | ||
|
|
03f54643fd | ||
|
|
7fb6d44208 | ||
|
|
dc683a50a4 | ||
|
|
be421054d8 | ||
|
|
fcef0391bb | ||
|
|
1d89aafeab | ||
|
|
7386a2deb6 | ||
|
|
26f5ebc8e2 |
@@ -14,3 +14,4 @@ dist-pkg
|
|||||||
pkg/harvester/index.ts
|
pkg/harvester/index.ts
|
||||||
pkg/harvester/store/*
|
pkg/harvester/store/*
|
||||||
scripts/pkgfile.js
|
scripts/pkgfile.js
|
||||||
|
tmp
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ description: Setup node and install dependencies
|
|||||||
runs:
|
runs:
|
||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
|
|||||||
@@ -3,5 +3,4 @@ addReviewers: true
|
|||||||
numberOfReviewers: 0
|
numberOfReviewers: 0
|
||||||
reviewers:
|
reviewers:
|
||||||
- a110605
|
- a110605
|
||||||
- houhoucoop
|
- houhoucoop
|
||||||
- NickChungSUSE
|
|
||||||
+12
-31
@@ -1,33 +1,14 @@
|
|||||||
pull_request_rules:
|
pull_request_rules:
|
||||||
- name: Automatically open backport PR to release-harvester-v1.5
|
- name: Ask to resolve conflict
|
||||||
conditions:
|
conditions:
|
||||||
- base=main
|
- conflict
|
||||||
- label="require backport 1.5"
|
actions:
|
||||||
actions:
|
comment:
|
||||||
backport:
|
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
|
||||||
branches:
|
|
||||||
- "release-harvester-v1.5"
|
|
||||||
assignees:
|
|
||||||
- "{{ author }}"
|
|
||||||
labels:
|
|
||||||
- "v1.5 backport PR"
|
|
||||||
|
|
||||||
- name: Automatically open backport PR to release-harvester-v1.0
|
commands_restrictions:
|
||||||
conditions:
|
backport:
|
||||||
- base=main
|
conditions:
|
||||||
- label="require backport 1.0"
|
- or:
|
||||||
actions:
|
- sender-permission>=write
|
||||||
backport:
|
- sender=github-actions[bot]
|
||||||
branches:
|
|
||||||
- "release-harvester-v1.0"
|
|
||||||
assignees:
|
|
||||||
- "{{ author }}"
|
|
||||||
labels:
|
|
||||||
- "v1.0 backport PR"
|
|
||||||
|
|
||||||
- name: Ask to resolve conflict
|
|
||||||
conditions:
|
|
||||||
- conflict
|
|
||||||
actions:
|
|
||||||
comment:
|
|
||||||
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# .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
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": ["config:recommended"],
|
"extends": [
|
||||||
|
"config:recommended",
|
||||||
|
":semanticCommitTypeAll(deps)",
|
||||||
|
":semanticCommitScopeDisabled"
|
||||||
|
],
|
||||||
"baseBranches": [
|
"baseBranches": [
|
||||||
"master",
|
|
||||||
"main",
|
"main",
|
||||||
"/^release-harvester-v\\d+\\.\\d+$/"
|
"/^release-harvester-v\\d+\\.\\d+$/"
|
||||||
],
|
],
|
||||||
"automergeMajor": false,
|
"automergeMajor": false,
|
||||||
"semanticCommits": "enabled",
|
"semanticCommits": "enabled",
|
||||||
"semanticCommitType": "chore",
|
"semanticCommitType": "deps",
|
||||||
"semanticCommitScope": "deps",
|
|
||||||
"prHourlyLimit": 12,
|
"prHourlyLimit": 12,
|
||||||
"timezone": "Asia/Taipei",
|
"timezone": "Asia/Taipei",
|
||||||
"schedule": ["after 10am on sunday"],
|
"schedule": ["after 10am on sunday"],
|
||||||
@@ -31,12 +33,14 @@
|
|||||||
{
|
{
|
||||||
"matchUpdateTypes": ["minor"],
|
"matchUpdateTypes": ["minor"],
|
||||||
"groupName": "minor dependencies",
|
"groupName": "minor dependencies",
|
||||||
|
"minimumReleaseAge": "7 days",
|
||||||
"labels": ["minor-update"],
|
"labels": ["minor-update"],
|
||||||
"reviewers": ["a110605", "houhoucoop"]
|
"reviewers": ["a110605", "houhoucoop"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchUpdateTypes": ["patch", "digest"],
|
"matchUpdateTypes": ["patch", "digest"],
|
||||||
"automerge": true,
|
"automerge": true,
|
||||||
|
"minimumReleaseAge": "7 days",
|
||||||
"groupName": "patch digest dependencies",
|
"groupName": "patch digest dependencies",
|
||||||
"labels": ["patch-update", "automerge"]
|
"labels": ["patch-update", "automerge"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ jobs:
|
|||||||
if: github.event.pull_request.draft == false
|
if: github.event.pull_request.draft == false
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: rancher/gh-issue-mgr/auto-assign-action@main
|
- uses: rancher/gh-issue-mgr/auto-assign-action@b70f0bdf12a03e5e3f33e4f92ccb6c89deb3ebd9 # main
|
||||||
with:
|
with:
|
||||||
configuration-path: .github/auto-assign-config.yaml
|
configuration-path: .github/auto-assign-config.yaml
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.base_ref }}
|
ref: ${{ github.base_ref }}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: "[PR Management] Request Backport via Mergify"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [closed]
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
comment-backport:
|
||||||
|
if: github.event.pull_request.merged == true
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.base_ref }}
|
||||||
|
|
||||||
|
- name: Post Mergify backport command
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
pr_number=${{ github.event.pull_request.number }}
|
||||||
|
repo="${{ github.repository }}"
|
||||||
|
|
||||||
|
labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
|
||||||
|
labels=$(echo "$labels_json" | jq -r '.[] // empty')
|
||||||
|
|
||||||
|
echo "Labels on PR: $labels"
|
||||||
|
|
||||||
|
matches=$(echo "$labels" | grep -oE '^require backport/v[0-9]+\.[0-9]+$' || true)
|
||||||
|
|
||||||
|
if [[ -z "$matches" ]]; then
|
||||||
|
echo "No back‑port labels found — skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
branches=$(echo "$matches" \
|
||||||
|
| sed -E 's/^require backport\/v/release-harvester-v/' \
|
||||||
|
| sort -u | tr '\n' ' ')
|
||||||
|
branches=${branches%% }
|
||||||
|
|
||||||
|
cmd="@Mergifyio backport $branches"
|
||||||
|
echo "Posting Mergify command: $cmd"
|
||||||
|
gh pr comment "$pr_number" --repo "$repo" --body "$cmd"
|
||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Check package version
|
- name: Check package version
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ jobs:
|
|||||||
name: Build & Upload Hosted
|
name: Build & Upload Hosted
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@@ -45,19 +45,20 @@ jobs:
|
|||||||
run: ./scripts/build-upload-gate
|
run: ./scripts/build-upload-gate
|
||||||
|
|
||||||
- name: Get gcs auth
|
- name: Get gcs auth
|
||||||
uses: rancher-eio/read-vault-secrets@main
|
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
||||||
with:
|
with:
|
||||||
secrets: |
|
secrets: |
|
||||||
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
||||||
|
|
||||||
- name: Apply gcs auth
|
- name: Apply gcs auth
|
||||||
# https://github.com/google-github-actions/auth
|
# https://github.com/google-github-actions/auth
|
||||||
uses: 'google-github-actions/auth@v2'
|
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
||||||
|
|
||||||
with:
|
with:
|
||||||
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
||||||
|
|
||||||
- name: Upload build
|
- name: Upload build
|
||||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
||||||
# https://github.com/google-github-actions/upload-cloud-storage
|
# https://github.com/google-github-actions/upload-cloud-storage
|
||||||
with:
|
with:
|
||||||
path: ${{steps.build-hosted.outputs.BUILD_HOSTED_DIR}}
|
path: ${{steps.build-hosted.outputs.BUILD_HOSTED_DIR}}
|
||||||
@@ -71,12 +72,12 @@ jobs:
|
|||||||
name: Build & Upload Embedded
|
name: Build & Upload Embedded
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@@ -89,19 +90,19 @@ jobs:
|
|||||||
DISABLED_EMBED_PKG: https://releases.rancher.com/harvester-ui/plugin/harvester-1.0.3.tar.gz
|
DISABLED_EMBED_PKG: https://releases.rancher.com/harvester-ui/plugin/harvester-1.0.3.tar.gz
|
||||||
|
|
||||||
- name: Get gcs auth
|
- name: Get gcs auth
|
||||||
uses: rancher-eio/read-vault-secrets@main
|
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
||||||
with:
|
with:
|
||||||
secrets: |
|
secrets: |
|
||||||
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
||||||
|
|
||||||
- name: Apply gcs auth
|
- name: Apply gcs auth
|
||||||
# https://github.com/google-github-actions/auth
|
# https://github.com/google-github-actions/auth
|
||||||
uses: 'google-github-actions/auth@v2'
|
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
||||||
with:
|
with:
|
||||||
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
||||||
|
|
||||||
- name: Upload tar
|
- name: Upload tar
|
||||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
||||||
with:
|
with:
|
||||||
path: ${{steps.build-embedded.outputs.BUILD_EMBEDED_TGZ}}
|
path: ${{steps.build-embedded.outputs.BUILD_EMBEDED_TGZ}}
|
||||||
destination: releases.rancher.com/harvester-ui/dashboard
|
destination: releases.rancher.com/harvester-ui/dashboard
|
||||||
@@ -114,12 +115,12 @@ jobs:
|
|||||||
name: Build & Upload Plugin
|
name: Build & Upload Plugin
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@@ -133,19 +134,19 @@ jobs:
|
|||||||
run: ./scripts/build-upload-gate
|
run: ./scripts/build-upload-gate
|
||||||
|
|
||||||
- name: Get gcs auth
|
- name: Get gcs auth
|
||||||
uses: rancher-eio/read-vault-secrets@main
|
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
||||||
with:
|
with:
|
||||||
secrets: |
|
secrets: |
|
||||||
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
||||||
|
|
||||||
- name: Apply gcs auth
|
- name: Apply gcs auth
|
||||||
# https://github.com/google-github-actions/auth
|
# https://github.com/google-github-actions/auth
|
||||||
uses: 'google-github-actions/auth@v2'
|
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
||||||
with:
|
with:
|
||||||
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
||||||
|
|
||||||
- name: Upload plugin tar
|
- name: Upload plugin tar
|
||||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
||||||
with:
|
with:
|
||||||
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_TARBALL}}
|
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_TARBALL}}
|
||||||
destination: releases.rancher.com/harvester-ui/plugin
|
destination: releases.rancher.com/harvester-ui/plugin
|
||||||
@@ -155,7 +156,7 @@ jobs:
|
|||||||
process_gcloudignore: false
|
process_gcloudignore: false
|
||||||
|
|
||||||
- name: Upload plugin directory
|
- name: Upload plugin directory
|
||||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
||||||
with:
|
with:
|
||||||
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_NAME}}
|
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_NAME}}
|
||||||
destination: releases.rancher.com/harvester-ui/plugin/${{steps.ci-build-pkg.outputs.PKG_NAME}}
|
destination: releases.rancher.com/harvester-ui/plugin/${{steps.ci-build-pkg.outputs.PKG_NAME}}
|
||||||
|
|||||||
@@ -27,14 +27,14 @@ jobs:
|
|||||||
build-status: ${{ job.status }}
|
build-status: ${{ job.status }}
|
||||||
steps:
|
steps:
|
||||||
- name: Read Secrets
|
- name: Read Secrets
|
||||||
uses: rancher-eio/read-vault-secrets@main
|
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
||||||
with:
|
with:
|
||||||
secrets: |
|
secrets: |
|
||||||
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | DOCKER_USERNAME ;
|
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | DOCKER_USERNAME ;
|
||||||
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD ;
|
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD ;
|
||||||
|
|
||||||
- name: Checkout repository (normal flow)
|
- name: Checkout repository (normal flow)
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Enable Corepack
|
- name: Enable Corepack
|
||||||
run: corepack enable
|
run: corepack enable
|
||||||
@@ -45,18 +45,18 @@ jobs:
|
|||||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||||
with:
|
with:
|
||||||
username: ${{ env.DOCKER_USERNAME }}
|
username: ${{ env.DOCKER_USERNAME }}
|
||||||
password: ${{ env.DOCKER_PASSWORD }}
|
password: ${{ env.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Setup Helm
|
- name: Setup Helm
|
||||||
uses: azure/setup-helm@v3
|
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
|
||||||
with:
|
with:
|
||||||
version: v3.8.0
|
version: v3.8.0
|
||||||
|
|
||||||
- name: Setup Nodejs with yarn caching
|
- name: Setup Nodejs with yarn caching
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20'
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -67,4 +67,4 @@ jobs:
|
|||||||
- name: Build and push UI image
|
- name: Build and push UI image
|
||||||
run: |
|
run: |
|
||||||
publish="yarn publish-pkgs -cp -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
|
publish="yarn publish-pkgs -cp -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
|
||||||
$publish
|
$publish
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
target_branch: ${{ steps.get-version.outputs.target_branch }}
|
target_branch: ${{ steps.get-version.outputs.target_branch }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Determine target branch
|
- name: Determine target branch
|
||||||
id: get-version
|
id: get-version
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
version: ${{ steps.get_version.outputs.version }}
|
version: ${{ steps.get_version.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Extract version from package.json
|
- name: Extract version from package.json
|
||||||
id: get_version
|
id: get_version
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Setup environment
|
- name: Setup environment
|
||||||
run: |
|
run: |
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
yarn install --frozen-lockfile
|
yarn install --frozen-lockfile
|
||||||
|
|
||||||
- name: Setup Helm
|
- name: Setup Helm
|
||||||
uses: azure/setup-helm@v3
|
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
|
||||||
with:
|
with:
|
||||||
version: v3.8.0
|
version: v3.8.0
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ jobs:
|
|||||||
yarn publish-pkgs -s ${{ github.repository }} -b ${{ needs.setup-target-branch.outputs.target_branch }} -t harvester-${{ needs.extract-version.outputs.version }}
|
yarn publish-pkgs -s ${{ github.repository }} -b ${{ needs.setup-target-branch.outputs.target_branch }} -t harvester-${{ needs.extract-version.outputs.version }}
|
||||||
|
|
||||||
- name: Upload charts artifact
|
- name: Upload charts artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
with:
|
with:
|
||||||
name: charts
|
name: charts
|
||||||
path: tmp
|
path: tmp
|
||||||
@@ -94,7 +94,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout release branch
|
- name: Checkout release branch
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
ref: '${{ github.ref_name }}'
|
ref: '${{ github.ref_name }}'
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ jobs:
|
|||||||
echo "LAST_COMMIT=${LAST_COMMIT}" >> $GITHUB_ENV
|
echo "LAST_COMMIT=${LAST_COMMIT}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout target branch
|
- name: Checkout target branch
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
ref: '${{ needs.setup-target-branch.outputs.target_branch }}'
|
ref: '${{ needs.setup-target-branch.outputs.target_branch }}'
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ jobs:
|
|||||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||||
|
|
||||||
- name: Download build artifacts
|
- name: Download build artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
with:
|
with:
|
||||||
name: charts
|
name: charts
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ jobs:
|
|||||||
git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
|
git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
|
||||||
|
|
||||||
- name: Run Helm chart releaser
|
- name: Run Helm chart releaser
|
||||||
uses: helm/chart-releaser-action@v1.4.1
|
uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0
|
||||||
with:
|
with:
|
||||||
charts_dir: ./charts
|
charts_dir: ./charts
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
release_tag: ${{ steps.determine_tag.outputs.release_tag }}
|
release_tag: ${{ steps.determine_tag.outputs.release_tag }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Determine release tag
|
- name: Determine release tag
|
||||||
id: determine_tag
|
id: determine_tag
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
- name: Check package version
|
- name: Check package version
|
||||||
env:
|
env:
|
||||||
TAG_VERSION: ${{ github.event.release.tag_name }}
|
TAG_VERSION: ${{ github.event.release.tag_name }}
|
||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- setup-release-tag
|
- setup-release-tag
|
||||||
- check-version
|
- check-version
|
||||||
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
|
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@9eb70a732e9be146722e1dbab431338366c2afc6 # creators-pkg-v3.0.10
|
||||||
permissions:
|
permissions:
|
||||||
actions: write
|
actions: write
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
- name: Check package version
|
- name: Check package version
|
||||||
env:
|
env:
|
||||||
TAG_VERSION: ${{github.ref_name}}
|
TAG_VERSION: ${{github.ref_name}}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: FOSSA Scanning
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main", "release-harvester-v*"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
fossa-scanning:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
|
# The FOSSA token is shared between all repos in Harvester's GH org. It can
|
||||||
|
# be used directly and there is no need to request specific access to EIO.
|
||||||
|
- name: Read FOSSA token
|
||||||
|
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
||||||
|
with:
|
||||||
|
secrets: |
|
||||||
|
secret/data/github/org/harvester/fossa/credentials token | FOSSA_API_KEY_PUSH_ONLY
|
||||||
|
|
||||||
|
- name: FOSSA scan
|
||||||
|
uses: fossas/fossa-action@ff70fe9fe17cbd2040648f1c45e8ec4e4884dcf3 # v1.9.0
|
||||||
|
with:
|
||||||
|
api-key: ${{ env.FOSSA_API_KEY_PUSH_ONLY }}
|
||||||
|
# Only runs the scan and do not provide/returns any results back to the
|
||||||
|
# pipeline.
|
||||||
|
run-tests: false
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: "[PR Management] Add PR Label"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, reopened, edited]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'release-harvester-v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-assign-pr-label:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.base_ref }}
|
||||||
|
- name: Setup Nodejs and yarn install
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
- name: Set PR label
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
|
||||||
|
echo "PR_LABEL = $PR_LABEL"
|
||||||
|
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "$PR_LABEL"
|
||||||
@@ -7,6 +7,7 @@ on:
|
|||||||
- main
|
- main
|
||||||
- 'release-*'
|
- 'release-*'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
types: [opened, reopened, edited, synchronize]
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- 'release-*'
|
- 'release-*'
|
||||||
@@ -15,13 +16,18 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Need full history for commit-lint
|
fetch-depth: 0 # Need full history for commit-lint
|
||||||
|
|
||||||
- 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
|
- name: Validate commit messages
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@@ -35,8 +41,14 @@ jobs:
|
|||||||
FROM="$GITHUB_BASE_SHA"
|
FROM="$GITHUB_BASE_SHA"
|
||||||
TO="$GITHUB_HEAD_SHA"
|
TO="$GITHUB_HEAD_SHA"
|
||||||
elif [ -n "$GITHUB_BEFORE" ] && [ -n "$GITHUB_AFTER" ]; then
|
elif [ -n "$GITHUB_BEFORE" ] && [ -n "$GITHUB_AFTER" ]; then
|
||||||
FROM="$GITHUB_BEFORE"
|
if [ "$GITHUB_BEFORE" = "0000000000000000000000000000000000000000" ]; then
|
||||||
TO="$GITHUB_AFTER"
|
# first push to HEAD
|
||||||
|
FROM=""
|
||||||
|
TO="$GITHUB_AFTER"
|
||||||
|
else
|
||||||
|
FROM="$GITHUB_BEFORE"
|
||||||
|
TO="$GITHUB_AFTER"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "No valid commit range found, skipping commitlint."
|
echo "No valid commit range found, skipping commitlint."
|
||||||
exit 0
|
exit 0
|
||||||
@@ -44,7 +56,15 @@ jobs:
|
|||||||
|
|
||||||
echo "FROM=$FROM"
|
echo "FROM=$FROM"
|
||||||
echo "TO=$TO"
|
echo "TO=$TO"
|
||||||
npx commitlint --from "$FROM" --to "$TO" --verbose
|
|
||||||
|
if [ -z "$FROM" ]; then
|
||||||
|
echo "Linting last commit $TO"
|
||||||
|
npx commitlint --last --verbose
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "Linting commits from $FROM to $TO"
|
||||||
|
npx commitlint --from "$FROM" --to "$TO" --verbose
|
||||||
|
fi
|
||||||
env:
|
env:
|
||||||
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
||||||
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
|||||||
@@ -138,7 +138,6 @@ To validate changes in a release series, switch to the appropriate `vX.Y-head` b
|
|||||||
- Examples:
|
- Examples:
|
||||||
- Test `1.0.x` series → `v1.0-head`
|
- Test `1.0.x` series → `v1.0-head`
|
||||||
- Test `1.5.x` series → `v1.5-head`
|
- Test `1.5.x` series → `v1.5-head`
|
||||||
- Test `1.6.x` series → `v1.6-head`
|
|
||||||
|
|
||||||
**Steps:**
|
**Steps:**
|
||||||
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
|
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
|
||||||
|
|||||||
@@ -22,12 +22,16 @@ module.exports = {
|
|||||||
'security', // Security fixes
|
'security', // Security fixes
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
// Allows the scope to be empty. [0] means the rule is disabled.
|
||||||
|
'scope-empty': [0, 'always'],
|
||||||
|
// Allows any string for the scope.
|
||||||
|
'scope-case': [2, 'always', 'kebab-case'],
|
||||||
'type-case': [2, 'always', 'lower-case'],
|
'type-case': [2, 'always', 'lower-case'],
|
||||||
'type-empty': [2, 'never'],
|
'type-empty': [2, 'never'],
|
||||||
'subject-case': [0, 'never'],
|
'subject-case': [0, 'never'],
|
||||||
'subject-empty': [2, 'never'],
|
'subject-empty': [2, 'never'],
|
||||||
'subject-full-stop': [2, 'never', '.'],
|
'subject-full-stop': [2, 'never', '.'],
|
||||||
'subject-max-length': [2, 'always', 72],
|
'subject-max-length': [0, 'never'],
|
||||||
'body-leading-blank': [2, 'always'],
|
'body-leading-blank': [2, 'always'],
|
||||||
'body-max-line-length': [2, 'always', 100],
|
'body-max-line-length': [2, 'always', 100],
|
||||||
'footer-leading-blank': [2, 'always'],
|
'footer-leading-blank': [2, 'always'],
|
||||||
|
|||||||
+12
-11
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.6.0-rc2",
|
"version": "1.7.2-rc3",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-class-static-block": "7.26.0",
|
"@babel/plugin-transform-class-static-block": "7.29.7",
|
||||||
"@rancher/shell": "3.0.5-rc.6",
|
"@rancher/shell": "3.0.8-rc.8",
|
||||||
"cache-loader": "^4.1.0",
|
"cache-loader": "^4.1.0",
|
||||||
"color": "4.2.3",
|
"color": "4.2.3",
|
||||||
"ip": "2.0.1",
|
"ip": "2.0.1",
|
||||||
@@ -16,21 +16,21 @@
|
|||||||
"yaml": "^2.5.1"
|
"yaml": "^2.5.1"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/node": "~20.10.0",
|
"@types/node": "~20.19.0",
|
||||||
"cronstrue": "2.50.0",
|
"cronstrue": "2.59.0",
|
||||||
"d3-color": "3.1.0",
|
"d3-color": "3.1.0",
|
||||||
"ejs": "3.1.10",
|
"ejs": "3.1.10",
|
||||||
"follow-redirects": "1.15.6",
|
"follow-redirects": "1.16.0",
|
||||||
"glob": "7.2.3",
|
"glob": "7.2.3",
|
||||||
"glob-parent": "6.0.2",
|
"glob-parent": "6.0.2",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"@types/lodash": "4.17.5",
|
"@types/lodash": "4.17.24",
|
||||||
"merge": "2.1.1",
|
"merge": "2.1.1",
|
||||||
"node-forge": "1.3.1",
|
"node-forge": "1.4.0",
|
||||||
"nth-check": "2.1.1",
|
"nth-check": "2.1.1",
|
||||||
"qs": "6.11.1",
|
"qs": "6.15.2",
|
||||||
"roarr": "7.0.4",
|
"roarr": "7.21.5",
|
||||||
"semver": "7.5.4",
|
"semver": "7.8.4",
|
||||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@commitlint/load": "^19.8.1",
|
||||||
"@commitlint/cli": "^19.8.1",
|
"@commitlint/cli": "^19.8.1",
|
||||||
"@commitlint/config-conventional": "^19.8.1",
|
"@commitlint/config-conventional": "^19.8.1",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
# harvester-ui-extension
|
||||||
|
|
||||||
|
The Harvester UI Extension is a Rancher extension that provides the user interface for [Harvester](https://harvesterhci.io) within the [Rancher Dashboard](https://github.com/rancher/dashboard).
|
||||||
|
|
||||||
|
> **Note:**
|
||||||
|
> This extension is available starting from **Rancher 2.10.0**. Ensure your Rancher version is **2.10.0 or later** to access Harvester integration.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
For detailed installation instructions, please refer to the [official Harvester documentation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210).
|
||||||
|
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
Ensure **Node.js v20 or later** is installed for development and debugging.
|
||||||
|
|
||||||
|
### Standalone Mode
|
||||||
|
|
||||||
|
Run the extension standalone with hot reload at `https://localhost:8005`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# Start the development server
|
||||||
|
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
||||||
|
|
||||||
|
# Example with specific server version
|
||||||
|
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.5.0 API=https://192.168.1.123 yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
You may also define environment variables in a `.env` file:
|
||||||
|
|
||||||
|
```env
|
||||||
|
RANCHER_ENV=harvester
|
||||||
|
VUE_APP_SERVER_VERSION=v1.5.0
|
||||||
|
API=https://192.168.1.123
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rancher Integration Mode
|
||||||
|
|
||||||
|
To run as a Rancher extension, follow the [Rancher UI Extension Guide](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
API=https://your-rancher-ip yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commit Message Guidelines
|
||||||
|
|
||||||
|
This project uses [commit-lint](https://commitlint.js.org/) with [Conventional Commits](https://www.conventionalcommits.org/) to ensure consistent and meaningful commit messages.
|
||||||
|
|
||||||
|
### Commit Message Format
|
||||||
|
|
||||||
|
All commit messages must follow the conventional commit format:
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>[optional scope]: <description>
|
||||||
|
|
||||||
|
[optional body]
|
||||||
|
|
||||||
|
[optional footer(s)]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supported Types
|
||||||
|
|
||||||
|
- **feat**: New features
|
||||||
|
- **fix**: Bug fixes
|
||||||
|
- **docs**: Documentation changes
|
||||||
|
- **style**: Code style changes (formatting, missing semicolons, etc.)
|
||||||
|
- **refactor**: Code refactoring
|
||||||
|
- **perf**: Performance improvements
|
||||||
|
- **test**: Adding or updating tests
|
||||||
|
- **build**: Build system or external dependencies
|
||||||
|
- **ci**: CI/CD changes
|
||||||
|
- **chore**: Other changes that don't modify src or test files
|
||||||
|
- **revert**: Reverts a previous commit
|
||||||
|
- **wip**: Work in progress
|
||||||
|
- **deps**: Dependency updates
|
||||||
|
- **security**: Security fixes
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Feature
|
||||||
|
git commit -m "feat: add new virtual machine creation wizard"
|
||||||
|
|
||||||
|
# Bug fix
|
||||||
|
git commit -m "fix: resolve memory leak in VM console"
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
git commit -m "docs: update installation instructions"
|
||||||
|
|
||||||
|
# Breaking change
|
||||||
|
git commit -m "feat!: change API endpoint structure
|
||||||
|
|
||||||
|
BREAKING CHANGE: The /api/v1/vms endpoint has been replaced with /api/v2/vms"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Git Hooks
|
||||||
|
|
||||||
|
The project uses [Husky](https://typicode.github.io/husky/) to automatically validate commit messages and run linting before commits:
|
||||||
|
|
||||||
|
- **pre-commit**: Runs ESLint to ensure code quality
|
||||||
|
- **commit-msg**: Validates commit message format using commit-lint
|
||||||
|
|
||||||
|
These hooks are automatically installed when you run `yarn install`.
|
||||||
|
|
||||||
|
### Manual Validation
|
||||||
|
|
||||||
|
You can manually validate commit messages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Validate the last commit
|
||||||
|
yarn commitlint
|
||||||
|
|
||||||
|
# Validate a specific commit
|
||||||
|
npx commitlint --from <commit-hash>
|
||||||
|
|
||||||
|
# Validate a range of commits
|
||||||
|
npx commitlint --from <start-hash> --to <end-hash>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Branch Structure
|
||||||
|
|
||||||
|
- **`main`** – Main development branch
|
||||||
|
- **`release-harvester-vX.Y`** – Stable release branches per version series
|
||||||
|
- **`vX.Y-head`** – Testing branches for ongoing changes to extension builds in each release series
|
||||||
|
|
||||||
|
> **Note:**
|
||||||
|
> The `vX.Y-head` branches are auto-generated and kept in sync with release branches. Use these for testing the latest changes in each version series.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
|
||||||
|
### UI Extension Testing
|
||||||
|
|
||||||
|
To validate changes in a release series, switch to the appropriate `vX.Y-head` branch. For main branch testing, use `main-head`.
|
||||||
|
|
||||||
|
- Examples:
|
||||||
|
- Test `1.0.x` series → `v1.0-head`
|
||||||
|
- Test `1.5.x` series → `v1.5-head`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
|
||||||
|
2. Refresh the Harvester repository using the target `vX.Y-head` branch
|
||||||
|
3. Go to the **Extensions** page and install the desired version
|
||||||
|
|
||||||
|
### Standalone Mode Testing
|
||||||
|
|
||||||
|
To test the standalone UI, configure Harvester to load the UI from an external source.
|
||||||
|
|
||||||
|
- Examples of `ui-index`:
|
||||||
|
- Main branch → `https://releases.rancher.com/harvester-ui/dashboard/latest/index.html`
|
||||||
|
- Release series `1.5.x` → `https://releases.rancher.com/harvester-ui/dashboard/release-harvester-v1.5/index.html`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Go to **Harvester UI** → **Advanced** → **Settings** → **UI**
|
||||||
|
2. Set **ui-source** to `External`
|
||||||
|
3. Set **ui-index** to the desired URL
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
If you want to contribute, start by reading this document, then visit our [Getting Started guide](https://extensions.rancher.io/extensions/next/extensions-getting-started) to learn how to develop and submit changes.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright (c) 2014-2025 [SUSE, LLC.](https://www.suse.com/)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -11,7 +11,6 @@ const CATEGORY = {
|
|||||||
ui: [
|
ui: [
|
||||||
'branding',
|
'branding',
|
||||||
'ui-source',
|
'ui-source',
|
||||||
'ui-plugin-index',
|
|
||||||
'ui-index',
|
'ui-index',
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@@ -98,8 +97,12 @@ export default {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || '';
|
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
|
// filter by description
|
||||||
if (description.includes(searchQuery)) {
|
if (description.includes(searchQuery)) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -257,8 +257,12 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
reconnect() {
|
||||||
|
this.$refs.novncConsole.reconnect();
|
||||||
|
},
|
||||||
|
|
||||||
softReboot() {
|
softReboot() {
|
||||||
this.vmResource.softrebootVM();
|
this.vmResource.doSoftReboot();
|
||||||
},
|
},
|
||||||
|
|
||||||
showKeysModal() {
|
showKeysModal() {
|
||||||
@@ -306,6 +310,13 @@ export default {
|
|||||||
{{ t("harvester.action.softreboot") }}
|
{{ t("harvester.action.softreboot") }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn-sm bg-primary"
|
||||||
|
@click="reconnect"
|
||||||
|
>
|
||||||
|
{{ t("harvester.action.reconnect") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
<v-dropdown
|
<v-dropdown
|
||||||
v-if="!hideCustomKeysBar"
|
v-if="!hideCustomKeysBar"
|
||||||
ref="customKeyPopover"
|
ref="customKeyPopover"
|
||||||
@@ -372,3 +383,15 @@ 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;
|
||||||
|
}
|
||||||
|
.vm-console .v-popper__inner{
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<script>
|
||||||
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { HCI_SETTING } from '../../config/settings';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterEditClusterRegistrationURL',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LabeledInput, MessageLink, Checkbox, Banner
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let parseDefaultValue = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
parseDefaultValue = JSON.parse(this.value.value);
|
||||||
|
} catch (error) {
|
||||||
|
parseDefaultValue.url = this.value.value;
|
||||||
|
parseDefaultValue.insecureSkipTLSVerify = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
parseDefaultValue,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
toCA() {
|
||||||
|
return `${ HCI_SETTING.ADDITIONAL_CA }?mode=edit`;
|
||||||
|
},
|
||||||
|
clusterRegistrationTLSVerifyEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('clusterRegistrationTLSVerify');
|
||||||
|
},
|
||||||
|
registrationURL: {
|
||||||
|
get() {
|
||||||
|
return this.clusterRegistrationTLSVerifyEnabled ? this.parseDefaultValue.url : this.parseDefaultValue;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
if (this.clusterRegistrationTLSVerifyEnabled) {
|
||||||
|
this.parseDefaultValue.url = value;
|
||||||
|
} else {
|
||||||
|
this.parseDefaultValue = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getDefaultValue() {
|
||||||
|
if (this.clusterRegistrationTLSVerifyEnabled) {
|
||||||
|
return { url: '', insecureSkipTLSVerify: false };
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUrl() {
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (this.clusterRegistrationTLSVerifyEnabled) {
|
||||||
|
this.value['value'] = JSON.stringify(this.parseDefaultValue);
|
||||||
|
} else {
|
||||||
|
this.value['value'] = this.parseDefaultValue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
this.parseDefaultValue = this.getDefaultValue();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateInsecureSkipTLSVerify(newValue) {
|
||||||
|
const { url = '' } = this.parseDefaultValue;
|
||||||
|
|
||||||
|
this.parseDefaultValue = { url, insecureSkipTLSVerify: newValue };
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div class="col span-12">
|
||||||
|
<Banner color="info">
|
||||||
|
<MessageLink
|
||||||
|
:to="toCA"
|
||||||
|
target="_blank"
|
||||||
|
prefix-label="harvester.setting.clusterRegistrationUrl.tip.prefix"
|
||||||
|
middle-label="harvester.setting.clusterRegistrationUrl.tip.middle"
|
||||||
|
suffix-label="harvester.setting.clusterRegistrationUrl.tip.suffix"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="registrationURL"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.setting.clusterRegistrationUrl.url')"
|
||||||
|
@update:value="updateUrl"
|
||||||
|
/>
|
||||||
|
<div v-if="clusterRegistrationTLSVerifyEnabled">
|
||||||
|
<Checkbox
|
||||||
|
v-model:value="parseDefaultValue.insecureSkipTLSVerify"
|
||||||
|
class="check mb-5"
|
||||||
|
type="checkbox"
|
||||||
|
:label="t('harvester.setting.clusterRegistrationUrl.insecureSkipTLSVerify')"
|
||||||
|
@update:value="updateInsecureSkipTLSVerify"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<script>
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
|
import { MEBIBYTE } from '../../utils/unit';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'KubevirtMigration',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LabeledInput,
|
||||||
|
UnitInput,
|
||||||
|
RadioGroup,
|
||||||
|
Banner
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
value: '',
|
||||||
|
default: '{}'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const migration = this.parseJSON(this.value?.value) || this.parseJSON(this.value?.default) || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
MEBIBYTE,
|
||||||
|
migration,
|
||||||
|
parseError: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.registerBeforeHook?.(this.willSave, 'willSave');
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
parseJSON(string) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(string);
|
||||||
|
} catch (e) {
|
||||||
|
this.parseError = this.t('kubevirtMigration.parseError', { error: e.message });
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Failed to parse JSON:', e.message);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateValue() {
|
||||||
|
if (this.value) {
|
||||||
|
this.value.value = JSON.stringify(this.migration);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
if (this.value?.default) {
|
||||||
|
const defaultMigration = this.parseJSON(this.value.default) || {};
|
||||||
|
|
||||||
|
this.migration = defaultMigration;
|
||||||
|
this.updateValue();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async willSave() {
|
||||||
|
this.updateValue();
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Banner
|
||||||
|
v-if="parseError"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{{ parseError }}
|
||||||
|
</Banner>
|
||||||
|
|
||||||
|
<div class="migration-field">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value.number="migration.parallelMigrationsPerCluster"
|
||||||
|
:label="t('harvester.setting.kubevirtMigration.parallelMigrationsPerCluster')"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value.number="migration.parallelOutboundMigrationsPerNode"
|
||||||
|
:label="t('harvester.setting.kubevirtMigration.parallelOutboundMigrationsPerNode')"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="migration.bandwidthPerMigration"
|
||||||
|
min="0"
|
||||||
|
:label="t('harvester.setting.kubevirtMigration.bandwidthPerMigration')"
|
||||||
|
:mode="mode"
|
||||||
|
:suffix="MEBIBYTE"
|
||||||
|
:tooltip="t('harvester.setting.kubevirtMigration.bandwidthPerMigrationTooltip', _, true)"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="migration.completionTimeoutPerGiB"
|
||||||
|
:label="t('harvester.setting.kubevirtMigration.completionTimeoutPerGiB')"
|
||||||
|
:mode="mode"
|
||||||
|
:suffix="migration.completionTimeoutPerGiB === 1 ? 'Second' : 'Seconds'"
|
||||||
|
min="10"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="migration.progressTimeout"
|
||||||
|
:label="t('harvester.setting.kubevirtMigration.progressTimeout')"
|
||||||
|
:mode="mode"
|
||||||
|
:suffix="migration.progressTimeout === 1 ? 'Second' : 'Seconds'"
|
||||||
|
min="10"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-for="field in ['allowAutoConverge','allowPostCopy','unsafeMigrationOverride','allowWorkloadDisruption','disableTLS','matchSELinuxLevelOnMigration']"
|
||||||
|
:key="field"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="mb-5"
|
||||||
|
:for="field"
|
||||||
|
>{{ t(`harvester.setting.kubevirtMigration.${field}`) }}</label>
|
||||||
|
<RadioGroup
|
||||||
|
:id="field"
|
||||||
|
v-model:value="migration[field]"
|
||||||
|
:options="[
|
||||||
|
{ label: t('advancedSettings.edit.trueOption'), value: true },
|
||||||
|
{ label: t('advancedSettings.edit.falseOption'), value: false },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.migration-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
<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>
|
||||||
@@ -18,16 +18,20 @@ export default {
|
|||||||
|
|
||||||
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
||||||
|
|
||||||
try {
|
if (this.customSupportBundleFeatureEnabled) {
|
||||||
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
try {
|
||||||
const response = await this.$store.dispatch('harvester/request', { url });
|
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
||||||
|
const response = await this.$store.dispatch('harvester/request', { url });
|
||||||
|
|
||||||
this.defaultNamespaces = response.data || [];
|
this.defaultNamespaces = response.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
this.defaultNamespaces = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
this.defaultNamespaces = [];
|
this.defaultNamespaces = [];
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -42,18 +46,30 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
customSupportBundleFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
|
||||||
|
},
|
||||||
|
|
||||||
allNamespaces() {
|
allNamespaces() {
|
||||||
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
||||||
},
|
},
|
||||||
|
|
||||||
filteredNamespaces() {
|
filteredNamespaces() {
|
||||||
|
if (!this.customSupportBundleFeatureEnabled) {
|
||||||
|
return this.allNamespaces;
|
||||||
|
}
|
||||||
|
|
||||||
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
|
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
|
||||||
|
|
||||||
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
|
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
|
||||||
},
|
},
|
||||||
|
|
||||||
namespaceOptions() {
|
namespaceOptions() {
|
||||||
if (this.availableNamespaces.length === 0) return [];
|
const mappedNamespaces = this.filteredNamespaces.map((ns) => ({ label: ns, value: ns }));
|
||||||
|
|
||||||
|
if (!this.customSupportBundleFeatureEnabled) {
|
||||||
|
return mappedNamespaces;
|
||||||
|
}
|
||||||
|
|
||||||
const allSelected =
|
const allSelected =
|
||||||
this.namespaces.length === this.filteredNamespaces.length &&
|
this.namespaces.length === this.filteredNamespaces.length &&
|
||||||
@@ -61,7 +77,7 @@ export default {
|
|||||||
|
|
||||||
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
|
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
|
||||||
|
|
||||||
return [controlOption, ...this.filteredNamespaces];
|
return [controlOption, ...mappedNamespaces];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
// suffix of doc link, see utils/feature-flags.js how to get complete doc link
|
// suffix of doc link, see utils/feature-flags.js how to get complete doc link
|
||||||
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`,
|
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
|
||||||
UPGRADE_URL: `/upgrade/index`,
|
UPGRADE_URL: `/upgrade/index`,
|
||||||
UPGRADE_CONFIG_URL: `/advanced/index#upgrade-config`,
|
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`,
|
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
|
||||||
|
VPC_CONFIGURATION_EXAMPLES: `/networking/kubeovn-vpc#vpc-peering-configuration-examples`,
|
||||||
|
NETWORK_POLICY: `/networking/kubeovn-vm-isolation/#network-policies`,
|
||||||
|
TRANSPARENT_HUGEPAGES: `https://docs.kernel.org/admin-guide/mm/transhuge.html`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,12 +39,27 @@ const FEATURE_FLAGS = {
|
|||||||
],
|
],
|
||||||
'v1.5.1': [],
|
'v1.5.1': [],
|
||||||
'v1.6.0': [
|
'v1.6.0': [
|
||||||
'vmMachineTypes',
|
|
||||||
'customSupportBundle',
|
'customSupportBundle',
|
||||||
'csiOnlineExpandValidation',
|
'csiOnlineExpandValidation',
|
||||||
'vmNetworkMigration',
|
'vmNetworkMigration',
|
||||||
'kubeovnVpcSubnet'
|
'kubeovnVpcSubnet',
|
||||||
]
|
'rancherClusterSetting',
|
||||||
|
'cpuMemoryHotplug',
|
||||||
|
'cdiSettings',
|
||||||
|
'vmCloneRunStrategy',
|
||||||
|
],
|
||||||
|
'v1.6.1': [],
|
||||||
|
'v1.7.0': [
|
||||||
|
'vmMachineTypeAuto',
|
||||||
|
'lhV2VolExpansion',
|
||||||
|
'l2VlanTrunkMode',
|
||||||
|
'kubevirtMigration',
|
||||||
|
'hotplugNic'
|
||||||
|
],
|
||||||
|
'v1.7.1': [],
|
||||||
|
'v1.7.2': [
|
||||||
|
'clusterRegistrationTLSVerify'
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateFeatureFlags = () => {
|
const generateFeatureFlags = () => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
LOGGING,
|
LOGGING,
|
||||||
STORAGE_CLASS,
|
STORAGE_CLASS,
|
||||||
SECRET,
|
SECRET,
|
||||||
|
NETWORK_POLICY
|
||||||
} from '@shell/config/types';
|
} from '@shell/config/types';
|
||||||
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
||||||
import {
|
import {
|
||||||
@@ -425,6 +426,7 @@ export function init($plugin, store) {
|
|||||||
HCI.CLUSTER_NETWORK,
|
HCI.CLUSTER_NETWORK,
|
||||||
HCI.NETWORK_ATTACHMENT,
|
HCI.NETWORK_ATTACHMENT,
|
||||||
HCI.VPC,
|
HCI.VPC,
|
||||||
|
NETWORK_POLICY,
|
||||||
HCI.LB,
|
HCI.LB,
|
||||||
HCI.IP_POOL,
|
HCI.IP_POOL,
|
||||||
],
|
],
|
||||||
@@ -455,6 +457,7 @@ export function init($plugin, store) {
|
|||||||
HCI.PCI_DEVICE,
|
HCI.PCI_DEVICE,
|
||||||
HCI.SR_IOVGPU_DEVICE,
|
HCI.SR_IOVGPU_DEVICE,
|
||||||
HCI.VGPU_DEVICE,
|
HCI.VGPU_DEVICE,
|
||||||
|
HCI.MIG_CONFIGURATION,
|
||||||
HCI.USB_DEVICE,
|
HCI.USB_DEVICE,
|
||||||
HCI.ADD_ONS,
|
HCI.ADD_ONS,
|
||||||
HCI.SECRET,
|
HCI.SECRET,
|
||||||
@@ -566,6 +569,21 @@ export function init($plugin, store) {
|
|||||||
ifHaveType: HCI.VPC,
|
ifHaveType: HCI.VPC,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
configureType(NETWORK_POLICY, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||||
|
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.networkPolicy.label',
|
||||||
|
name: NETWORK_POLICY,
|
||||||
|
namespaced: true,
|
||||||
|
weight: 186,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: NETWORK_POLICY }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: NETWORK_POLICY,
|
||||||
|
});
|
||||||
|
|
||||||
configureType(HCI.SNAPSHOT, {
|
configureType(HCI.SNAPSHOT, {
|
||||||
isCreatable: false,
|
isCreatable: false,
|
||||||
location: {
|
location: {
|
||||||
@@ -832,6 +850,26 @@ export function init($plugin, store) {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.migconfiguration.label',
|
||||||
|
group: 'advanced',
|
||||||
|
weight: 12,
|
||||||
|
name: HCI.MIG_CONFIGURATION,
|
||||||
|
namespaced: false,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.MIG_CONFIGURATION }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.MIG_CONFIGURATION,
|
||||||
|
});
|
||||||
|
|
||||||
|
configureType(HCI.MIG_CONFIGURATION, {
|
||||||
|
isCreatable: false,
|
||||||
|
hiddenNamespaceGroupButton: true,
|
||||||
|
canYaml: false,
|
||||||
|
});
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
labelKey: 'harvester.usb.label',
|
labelKey: 'harvester.usb.label',
|
||||||
group: 'advanced',
|
group: 'advanced',
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
// TODO: delete this not used variable
|
// TODO: delete this not used variable
|
||||||
export const MemoryUnit = [{
|
export const MemoryUnit = [{
|
||||||
label: 'Mi',
|
label: 'Mi',
|
||||||
@@ -81,3 +83,14 @@ 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
|
||||||
|
};
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export const HCI = {
|
|||||||
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
|
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
|
||||||
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
|
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
|
||||||
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
|
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
|
||||||
|
ADDON_DISPLAYNAME: 'addon.harvesterhci.io/displayName',
|
||||||
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
|
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
|
||||||
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
|
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
|
||||||
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
|
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
|
||||||
@@ -70,5 +71,11 @@ export const HCI = {
|
|||||||
K8S_ARCH: 'kubernetes.io/arch',
|
K8S_ARCH: 'kubernetes.io/arch',
|
||||||
IMAGE_DISPLAY_NAME: 'harvesterhci.io/imageDisplayName',
|
IMAGE_DISPLAY_NAME: 'harvesterhci.io/imageDisplayName',
|
||||||
CUSTOM_IP: 'harvesterhci.io/custom-ip',
|
CUSTOM_IP: 'harvesterhci.io/custom-ip',
|
||||||
IMPORTED_IMAGE: 'migration.harvesterhci.io/imported'
|
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',
|
||||||
|
MAC_ADDRESS: 'harvesterhci.io/mac-address',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ export const HCI_SETTING = {
|
|||||||
LOG_LEVEL: 'log-level',
|
LOG_LEVEL: 'log-level',
|
||||||
SERVER_VERSION: 'server-version',
|
SERVER_VERSION: 'server-version',
|
||||||
UI_INDEX: 'ui-index',
|
UI_INDEX: 'ui-index',
|
||||||
UI_PLUGIN_INDEX: 'ui-plugin-index',
|
|
||||||
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
|
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
|
||||||
UPGRADE_CHECKER_URL: 'upgrade-checker-url',
|
UPGRADE_CHECKER_URL: 'upgrade-checker-url',
|
||||||
VLAN: 'vlan',
|
VLAN: 'vlan',
|
||||||
@@ -37,6 +36,9 @@ export const HCI_SETTING = {
|
|||||||
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
|
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
|
||||||
UPGRADE_CONFIG: 'upgrade-config',
|
UPGRADE_CONFIG: 'upgrade-config',
|
||||||
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
||||||
|
RANCHER_CLUSTER: 'rancher-cluster',
|
||||||
|
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
|
||||||
|
KUBEVIRT_MIGRATION: 'kubevirt-migration'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_ALLOWED_SETTINGS = {
|
export const HCI_ALLOWED_SETTINGS = {
|
||||||
@@ -82,7 +84,6 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
[HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES]: { from: 'import', canReset: true },
|
[HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES]: { from: 'import', canReset: true },
|
||||||
[HCI_SETTING.AUTO_DISK_PROVISION_PATHS]: { canReset: true },
|
[HCI_SETTING.AUTO_DISK_PROVISION_PATHS]: { canReset: true },
|
||||||
[HCI_SETTING.RELEASE_DOWNLOAD_URL]: { kind: 'url' },
|
[HCI_SETTING.RELEASE_DOWNLOAD_URL]: { kind: 'url' },
|
||||||
[HCI_SETTING.UI_PLUGIN_INDEX]: { kind: 'url' },
|
|
||||||
[HCI_SETTING.CONTAINERD_REGISTRY]: {
|
[HCI_SETTING.CONTAINERD_REGISTRY]: {
|
||||||
kind: 'json', from: 'import', canReset: true
|
kind: 'json', from: 'import', canReset: true
|
||||||
},
|
},
|
||||||
@@ -108,14 +109,22 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
featureFlag: 'upgradeConfigSetting',
|
featureFlag: 'upgradeConfigSetting',
|
||||||
docPath: 'UPGRADE_CONFIG_URL'
|
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]: {
|
[HCI_SETTING.VM_MIGRATION_NETWORK]: {
|
||||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration',
|
kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration',
|
||||||
},
|
},
|
||||||
|
[HCI_SETTING.KUBEVIRT_MIGRATION]: {
|
||||||
|
kind: 'json', from: 'import', canReset: true, featureFlag: 'kubevirtMigration',
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
||||||
[HCI_SETTING.CLUSTER_REGISTRATION_URL]: {
|
[HCI_SETTING.CLUSTER_REGISTRATION_URL]: {
|
||||||
kind: 'url',
|
kind: 'custom',
|
||||||
|
from: 'import',
|
||||||
canReset: true,
|
canReset: true,
|
||||||
},
|
},
|
||||||
[HCI_SETTING.UI_PL]: {
|
[HCI_SETTING.UI_PL]: {
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ export const BACKUP_TYPE = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const NETWORK_TYPE = {
|
export const NETWORK_TYPE = {
|
||||||
L2VLAN: 'L2VlanNetwork',
|
L2VLAN: 'L2VlanNetwork',
|
||||||
UNTAGGED: 'UntaggedNetwork',
|
UNTAGGED: 'UntaggedNetwork',
|
||||||
OVERLAY: 'OverlayNetwork',
|
OVERLAY: 'OverlayNetwork',
|
||||||
|
L2TRUNK_VLAN: 'L2VlanTrunkNetwork',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VOLUME_MODE = {
|
export const VOLUME_MODE = {
|
||||||
@@ -18,3 +19,13 @@ export const NETWORK_PROTOCOL = {
|
|||||||
IPv4: 'IPv4',
|
IPv4: 'IPv4',
|
||||||
IPv6: 'IPv6',
|
IPv6: 'IPv6',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const INTERNAL_STORAGE_CLASS = {
|
||||||
|
VMSTATE_PERSISTENCE: 'vmstate-persistence',
|
||||||
|
LONGHORN_STATIC: 'longhorn-static',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const L2VLAN_MODE = {
|
||||||
|
ACCESS: 'access',
|
||||||
|
TRUNK: 'trunk',
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<script>
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import SortableTable from '@shell/components/SortableTable';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ResourceTabs,
|
||||||
|
Tab,
|
||||||
|
SortableTable,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'profileName',
|
||||||
|
labelKey: 'harvester.migconfiguration.tableHeaders.profileName',
|
||||||
|
value: 'name',
|
||||||
|
width: 75,
|
||||||
|
sort: 'name',
|
||||||
|
dashIfEmpty: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'vGPUID',
|
||||||
|
labelKey: 'harvester.migconfiguration.tableHeaders.vGPUID',
|
||||||
|
value: 'vGPUID',
|
||||||
|
width: 75,
|
||||||
|
sort: 'vGPUID',
|
||||||
|
dashIfEmpty: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'available',
|
||||||
|
labelKey: 'harvester.migconfiguration.tableHeaders.available',
|
||||||
|
value: 'available',
|
||||||
|
width: 75,
|
||||||
|
sort: 'available',
|
||||||
|
align: 'center',
|
||||||
|
dashIfEmpty: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'requested',
|
||||||
|
labelKey: 'harvester.migconfiguration.tableHeaders.requested',
|
||||||
|
value: 'requested',
|
||||||
|
width: 75,
|
||||||
|
sort: 'requested',
|
||||||
|
align: 'center',
|
||||||
|
dashIfEmpty: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'total',
|
||||||
|
labelKey: 'harvester.migconfiguration.tableHeaders.total',
|
||||||
|
value: 'total',
|
||||||
|
width: 75,
|
||||||
|
sort: 'total',
|
||||||
|
align: 'center',
|
||||||
|
dashIfEmpty: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
rows() {
|
||||||
|
let out = (this.value?.status?.profileStatus || []).map((profile) => {
|
||||||
|
const {
|
||||||
|
id, name, total, available
|
||||||
|
} = profile;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
total,
|
||||||
|
available,
|
||||||
|
vGPUID: profile.vGPUID?.join(', ') || '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
out = out.map((row) => {
|
||||||
|
const requested = this.value?.spec?.profileSpec.find((p) => p.id === row.id)?.requested || 0;
|
||||||
|
|
||||||
|
return { ...row, requested };
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ResourceTabs
|
||||||
|
:value="value"
|
||||||
|
:need-events="false"
|
||||||
|
:need-related="false"
|
||||||
|
:mode="mode"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="Profile Status"
|
||||||
|
:label="t('harvester.migconfiguration.profileStatus')"
|
||||||
|
>
|
||||||
|
<SortableTable
|
||||||
|
:headers="headers"
|
||||||
|
:rows="rows"
|
||||||
|
key-field="condition"
|
||||||
|
default-sort-by="condition"
|
||||||
|
:table-actions="false"
|
||||||
|
:row-actions="false"
|
||||||
|
:search="false"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</ResourceTabs>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<script>
|
||||||
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import { DOC } from '../../config/doc-links';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterHugepages',
|
||||||
|
components: { LabelValue },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
node: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
docsTransparentHugepagesLink() {
|
||||||
|
return DOC.TRANSPARENT_HUGEPAGES;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
|
||||||
|
|
||||||
|
this.hugepages = hash.find((node) => {
|
||||||
|
return node.id === this.node.id;
|
||||||
|
}) || {};
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { hugepages: {} };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<template v-if="hugepages.status">
|
||||||
|
<h2>{{ t('harvester.host.hugepages.meminfo') }}</h2>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.status.anon')"
|
||||||
|
:value="hugepages.status.meminfo.anonHugePages"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.status.size')"
|
||||||
|
:value="hugepages.status.meminfo.hugepageSize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.status.total')"
|
||||||
|
:value="hugepages.status.meminfo.hugePagesTotal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.status.free')"
|
||||||
|
:value="hugepages.status.meminfo.hugePagesFree"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.status.rsvd')"
|
||||||
|
:value="hugepages.status.meminfo.hugePagesRsvd"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.status.surp')"
|
||||||
|
:value="hugepages.status.meminfo.hugePagesSurp"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<hr class="divider" />
|
||||||
|
<h3>
|
||||||
|
<t
|
||||||
|
k="harvester.host.hugepages.transparent.title"
|
||||||
|
:raw="true"
|
||||||
|
:url="docsTransparentHugepagesLink"
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.transparent.enabled')"
|
||||||
|
:value="hugepages.spec.transparent.enabled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.transparent.shmemEnabled')"
|
||||||
|
:value="hugepages.spec.transparent.shmemEnabled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.transparent.defrag')"
|
||||||
|
:value="hugepages.spec.transparent.defrag"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -47,20 +47,26 @@ export default {
|
|||||||
STATE,
|
STATE,
|
||||||
NAME,
|
NAME,
|
||||||
{
|
{
|
||||||
name: 'vmCPU',
|
name: 'CPU',
|
||||||
labelKey: 'tableHeaders.cpu',
|
label: 'CPU',
|
||||||
search: false,
|
sort: ['displayCPU'],
|
||||||
sort: ['spec.template.spec.domain.cpu.cores'],
|
value: 'displayCPU',
|
||||||
value: 'spec.template.spec.domain.cpu.cores',
|
align: 'center',
|
||||||
width: 120
|
dashIfEmpty: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'vmRAM',
|
name: 'Memory',
|
||||||
labelKey: 'glance.memory',
|
value: 'displayMemory',
|
||||||
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',
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import Instance from './VirtualMachineInstance';
|
|||||||
import Disk from './HarvesterHostDisk';
|
import Disk from './HarvesterHostDisk';
|
||||||
import VlanStatus from './VlanStatus';
|
import VlanStatus from './VlanStatus';
|
||||||
import HarvesterKsmtuned from './HarvesterKsmtuned.vue';
|
import HarvesterKsmtuned from './HarvesterKsmtuned.vue';
|
||||||
|
import HarvesterHugepages from './HarvesterHugepages.vue';
|
||||||
import HarvesterSeeder from './HarvesterSeeder';
|
import HarvesterSeeder from './HarvesterSeeder';
|
||||||
|
|
||||||
const LONGHORN_SYSTEM = 'longhorn-system';
|
const LONGHORN_SYSTEM = 'longhorn-system';
|
||||||
@@ -46,6 +47,7 @@ export default {
|
|||||||
VlanStatus,
|
VlanStatus,
|
||||||
LabelValue,
|
LabelValue,
|
||||||
HarvesterKsmtuned,
|
HarvesterKsmtuned,
|
||||||
|
HarvesterHugepages,
|
||||||
Loading,
|
Loading,
|
||||||
SortableTable,
|
SortableTable,
|
||||||
HarvesterSeeder,
|
HarvesterSeeder,
|
||||||
@@ -209,6 +211,12 @@ export default {
|
|||||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hasHugepagesSchema() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.HUGEPAGES);
|
||||||
|
},
|
||||||
|
|
||||||
hasBlockDevicesSchema() {
|
hasBlockDevicesSchema() {
|
||||||
return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE);
|
return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE);
|
||||||
},
|
},
|
||||||
@@ -468,6 +476,16 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
v-if="hasHugepagesSchema"
|
||||||
|
name="hugepages"
|
||||||
|
:weight="0"
|
||||||
|
:show-header="false"
|
||||||
|
:label="t('harvester.host.tabs.hugepages')"
|
||||||
|
>
|
||||||
|
<HarvesterHugepages :node="value" />
|
||||||
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
v-if="seederEnabled"
|
v-if="seederEnabled"
|
||||||
name="seeder"
|
name="seeder"
|
||||||
|
|||||||
@@ -172,6 +172,9 @@ 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">
|
||||||
|
|||||||
@@ -172,6 +172,9 @@ 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">
|
||||||
|
|||||||
+3
-2
@@ -5,6 +5,7 @@ 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';
|
||||||
|
|
||||||
@@ -91,9 +92,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
flavor() {
|
flavor() {
|
||||||
const domain = this.value?.spec?.template?.spec?.domain;
|
const { cpu, memory } = getVmCPUMemoryValues(this.value);
|
||||||
|
|
||||||
return `${ domain.cpu?.cores } vCPU , ${ domain.resources?.limits?.memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
|
return `${ cpu } vCPU , ${ memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
|
||||||
},
|
},
|
||||||
|
|
||||||
kernelRelease() {
|
kernelRelease() {
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ export default {
|
|||||||
const diskRows = this.getDiskRows(neu);
|
const diskRows = this.getDiskRows(neu);
|
||||||
|
|
||||||
this['diskRows'] = diskRows;
|
this['diskRows'] = diskRows;
|
||||||
|
this['networkRows'] = this.getNetworkRows(neu, { fromTemplate: false, init: false });
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
}
|
}
|
||||||
@@ -265,6 +266,7 @@ export default {
|
|||||||
<Network
|
<Network
|
||||||
v-model:value="networkRows"
|
v-model:value="networkRows"
|
||||||
mode="view"
|
mode="view"
|
||||||
|
:vm="value"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<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',
|
||||||
@@ -14,7 +15,7 @@ export default {
|
|||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
AsyncButton, Banner, Checkbox, Card, LabeledInput
|
AsyncButton, Banner, Checkbox, Card, LabeledInput, LabeledSelect
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -26,9 +27,11 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
name: '',
|
name: '',
|
||||||
cloneData: true,
|
cloneData: true,
|
||||||
errors: []
|
errors: [],
|
||||||
|
runStrategy: runStrategyOptions[1],
|
||||||
|
runStrategyOptions
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -38,6 +41,9 @@ export default {
|
|||||||
actionResource() {
|
actionResource() {
|
||||||
return this.resources[0];
|
return this.resources[0];
|
||||||
},
|
},
|
||||||
|
vmCloneRunStrategyEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('vmCloneRunStrategy');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -58,7 +64,8 @@ export default {
|
|||||||
|
|
||||||
// deep clone
|
// deep clone
|
||||||
try {
|
try {
|
||||||
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
|
const payload = this.vmCloneRunStrategyEnabled ? { targetVm: this.name, runStrategy: this.runStrategy } : { targetVm: this.name };
|
||||||
|
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', {
|
||||||
@@ -106,6 +113,13 @@ 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
|
<Banner
|
||||||
v-for="(err, i) in errors"
|
v-for="(err, i) in errors"
|
||||||
:key="i"
|
:key="i"
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default {
|
|||||||
resources: {
|
resources: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -43,7 +43,7 @@ export default {
|
|||||||
...mapState('action-menu', ['modalData']),
|
...mapState('action-menu', ['modalData']),
|
||||||
|
|
||||||
title() {
|
title() {
|
||||||
return this.modalData.title || 'dialog.promptRemove.title';
|
return this.modalData?.title || 'dialog.promptRemove.title';
|
||||||
},
|
},
|
||||||
|
|
||||||
formattedType() {
|
formattedType() {
|
||||||
@@ -51,7 +51,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
warningMessage() {
|
warningMessage() {
|
||||||
if (this.modalData.warningMessage) return this.modalData.warningMessage;
|
if (this.modalData?.warningMessage) return this.modalData.warningMessage;
|
||||||
|
|
||||||
const isPlural = this.type.endsWith('s');
|
const isPlural = this.type.endsWith('s');
|
||||||
const thisOrThese = isPlural ? 'these' : 'this';
|
const thisOrThese = isPlural ? 'these' : 'this';
|
||||||
@@ -145,6 +145,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
for (const resource of this.resources) {
|
for (const resource of this.resources) {
|
||||||
await resource.remove();
|
await resource.remove();
|
||||||
|
if (this.modalData?.extraActionAfterRemove) await this.modalData.extraActionAfterRemove();
|
||||||
}
|
}
|
||||||
buttonDone(true);
|
buttonDone(true);
|
||||||
this.close();
|
this.close();
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
<script>
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
import { NETWORK_TYPE } from '../config/types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AddHotplugNic',
|
||||||
|
|
||||||
|
emits: ['close'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton,
|
||||||
|
Card,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
Banner
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
try {
|
||||||
|
this.allVMNetworks = await this.$store.dispatch('harvester/findAll', { type: NETWORK_ATTACHMENT });
|
||||||
|
} catch (err) {
|
||||||
|
this.errors = exceptionToErrorsArray(err);
|
||||||
|
this.allVMNetworks = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
interfaceName: '',
|
||||||
|
networkName: '',
|
||||||
|
macAddress: '',
|
||||||
|
allVMNetworks: [],
|
||||||
|
errors: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources?.[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
isFormValid() {
|
||||||
|
return this.interfaceName !== '' && this.networkName !== '';
|
||||||
|
},
|
||||||
|
|
||||||
|
vmNetworksOption() {
|
||||||
|
return this.allVMNetworks
|
||||||
|
.filter((network) => {
|
||||||
|
const labels = network.metadata?.labels || {};
|
||||||
|
const type = labels[HCI_ANNOTATIONS.NETWORK_TYPE];
|
||||||
|
|
||||||
|
const isValidType = [
|
||||||
|
NETWORK_TYPE.L2VLAN,
|
||||||
|
NETWORK_TYPE.UNTAGGED,
|
||||||
|
NETWORK_TYPE.L2TRUNK_VLAN,
|
||||||
|
].includes(type);
|
||||||
|
|
||||||
|
return isValidType && !network.isSystem;
|
||||||
|
})
|
||||||
|
.map((network) => {
|
||||||
|
const label = network.isNotReady ? `${ network.id } (${ this.t('generic.notReady') })` : network.id;
|
||||||
|
|
||||||
|
return ({
|
||||||
|
label,
|
||||||
|
value: network.id || '',
|
||||||
|
disabled: network.isNotReady,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.interfaceName = '';
|
||||||
|
this.networkName = '';
|
||||||
|
this.macAddress = '';
|
||||||
|
this.errors = [];
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(buttonCb) {
|
||||||
|
if (!this.actionResource) {
|
||||||
|
buttonCb(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
interfaceName: this.interfaceName,
|
||||||
|
networkName: this.networkName
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.macAddress) {
|
||||||
|
payload.macAddress = this.macAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await this.actionResource.doAction('addNic', payload);
|
||||||
|
|
||||||
|
if ([200, 204].includes(res?._status)) {
|
||||||
|
this.$store.dispatch('growl/success', {
|
||||||
|
title: this.t('generic.notification.title.succeed'),
|
||||||
|
message: this.t('harvester.modal.hotplugNic.success', {
|
||||||
|
interfaceName: this.interfaceName,
|
||||||
|
vm: this.actionResource.nameDisplay
|
||||||
|
})
|
||||||
|
}, { root: true });
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
buttonCb(true);
|
||||||
|
} else {
|
||||||
|
this.errors = exceptionToErrorsArray(res);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.errors = exceptionToErrorsArray(err);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card
|
||||||
|
ref="modal"
|
||||||
|
name="modal"
|
||||||
|
:show-highlight-border="false"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<h4
|
||||||
|
v-clean-html="t('harvester.modal.hotplugNic.title')"
|
||||||
|
class="text-default-text"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="interfaceName"
|
||||||
|
:label="t('generic.name')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="networkName"
|
||||||
|
class="mt-20"
|
||||||
|
:label="t('harvester.modal.hotplugNic.vmNetwork')"
|
||||||
|
:options="vmNetworksOption"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="macAddress"
|
||||||
|
class="mt-20"
|
||||||
|
label-key="harvester.modal.hotplugNic.macAddress"
|
||||||
|
:tooltip="t('harvester.modal.hotplugNic.macAddressTooltip', _, true)"
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
:label="err"
|
||||||
|
color="error"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<div class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn role-secondary mr-10"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
mode="apply"
|
||||||
|
:disabled="!isFormValid"
|
||||||
|
@click="save"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+4
-4
@@ -11,7 +11,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
|||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HotplugModal',
|
name: 'HotplugVolumeModal',
|
||||||
|
|
||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !pvc.attachVM;
|
return true;
|
||||||
})
|
})
|
||||||
.map((pvc) => {
|
.map((pvc) => {
|
||||||
return {
|
return {
|
||||||
@@ -90,7 +90,7 @@ export default {
|
|||||||
if (res._status === 200 || res._status === 204) {
|
if (res._status === 200 || res._status === 204) {
|
||||||
this.$store.dispatch('growl/success', {
|
this.$store.dispatch('growl/success', {
|
||||||
title: this.t('generic.notification.title.succeed'),
|
title: this.t('generic.notification.title.succeed'),
|
||||||
message: this.t('harvester.modal.hotplug.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
|
message: this.t('harvester.modal.hotplugVolume.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
|
||||||
}, { root: true });
|
}, { root: true });
|
||||||
|
|
||||||
this.close();
|
this.close();
|
||||||
@@ -122,7 +122,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<h4
|
<h4
|
||||||
v-clean-html="t('harvester.modal.hotplug.title')"
|
v-clean-html="t('harvester.modal.hotplugVolume.title')"
|
||||||
class="text-default-text"
|
class="text-default-text"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
<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,6 +10,7 @@ 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',
|
||||||
@@ -105,11 +106,17 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const options = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
|
const options = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
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,
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
|
|||||||
+38
-12
@@ -1,13 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapState, mapGetters } from 'vuex';
|
import { mapState, mapGetters } from 'vuex';
|
||||||
|
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
import { Card } from '@components/Card';
|
import { Card } from '@components/Card';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterHotUnplugModal',
|
name: 'HarvesterHotUnplug',
|
||||||
|
|
||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
|
|
||||||
@@ -35,8 +34,25 @@ export default {
|
|||||||
actionResource() {
|
actionResource() {
|
||||||
return this.resources[0];
|
return this.resources[0];
|
||||||
},
|
},
|
||||||
diskName() {
|
|
||||||
return this.modalData.diskName;
|
name() {
|
||||||
|
return this.modalData.name;
|
||||||
|
},
|
||||||
|
|
||||||
|
isVolume() {
|
||||||
|
return this.modalData.type === 'volume';
|
||||||
|
},
|
||||||
|
|
||||||
|
titleKey() {
|
||||||
|
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.title' : 'harvester.virtualMachine.hotUnplug.detachNIC.title';
|
||||||
|
},
|
||||||
|
|
||||||
|
actionLabelKey() {
|
||||||
|
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabel' : 'harvester.virtualMachine.hotUnplug.detachNIC.actionLabel';
|
||||||
|
},
|
||||||
|
|
||||||
|
successMessageKey() {
|
||||||
|
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.success' : 'harvester.virtualMachine.hotUnplug.detachNIC.success';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -47,14 +63,20 @@ export default {
|
|||||||
|
|
||||||
async save(buttonCb) {
|
async save(buttonCb) {
|
||||||
try {
|
try {
|
||||||
const res = await this.actionResource.doAction('removeVolume', { diskName: this.diskName });
|
let res;
|
||||||
|
|
||||||
|
if (this.isVolume) {
|
||||||
|
res = await this.actionResource.doAction('removeVolume', { diskName: this.name });
|
||||||
|
} else {
|
||||||
|
res = await this.actionResource.doAction('removeNic', { interfaceName: this.name });
|
||||||
|
}
|
||||||
|
|
||||||
if (res._status === 200 || res._status === 204) {
|
if (res._status === 200 || res._status === 204) {
|
||||||
this.$store.dispatch(
|
this.$store.dispatch(
|
||||||
'growl/success',
|
'growl/success',
|
||||||
{
|
{
|
||||||
title: this.t('generic.notification.title.succeed'),
|
title: this.t('generic.notification.title.succeed'),
|
||||||
message: this.t('harvester.modal.hotunplug.success', { name: this.diskName })
|
message: this.t(this.successMessageKey, { name: this.name })
|
||||||
},
|
},
|
||||||
{ root: true }
|
{ root: true }
|
||||||
);
|
);
|
||||||
@@ -64,14 +86,14 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||||
|
|
||||||
this['errors'] = error;
|
this.errors = error;
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err?.data || err;
|
const error = err?.data || err;
|
||||||
const message = exceptionToErrorsArray(error);
|
const message = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
this['errors'] = message;
|
this.errors = message;
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +109,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<h4
|
<h4
|
||||||
v-clean-html="t('harvester.virtualMachine.unplug.title', { name: diskName })"
|
v-clean-html="t(titleKey, { name })"
|
||||||
class="text-default-text"
|
class="text-default-text"
|
||||||
/>
|
/>
|
||||||
<Banner
|
<Banner
|
||||||
@@ -111,9 +133,9 @@ export default {
|
|||||||
|
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
mode="apply"
|
mode="apply"
|
||||||
:action-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
:action-label="t(actionLabelKey)"
|
||||||
:waiting-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
:waiting-label="t(actionLabelKey)"
|
||||||
:success-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
:success-label="t(actionLabelKey)"
|
||||||
@click="save"
|
@click="save"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,4 +154,8 @@ export default {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep(.card-title) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<script>
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterEditMIGConfiguration',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Tab,
|
||||||
|
Tabbed,
|
||||||
|
CruResource,
|
||||||
|
LabeledInput,
|
||||||
|
NameNsDescription,
|
||||||
|
LabelValue
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const { profileSpec } = this.value.spec;
|
||||||
|
|
||||||
|
return { profileSpec: profileSpec || [] };
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.updateBeforeSave);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isView() {
|
||||||
|
return this.mode === 'view';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateBeforeSave() {
|
||||||
|
// MIGConfiguration CRD don't have any namespace field,
|
||||||
|
// so we need to remove the namespace field before saving
|
||||||
|
delete this.value.metadata.namespace;
|
||||||
|
// enable the MIGConfiguration when saving
|
||||||
|
this.value.spec.enabled = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
labelTitle(profile) {
|
||||||
|
return `${ profile.name } (available : ${ this.available(profile) })`;
|
||||||
|
},
|
||||||
|
|
||||||
|
available(profile) {
|
||||||
|
const count = this.value.status?.profileStatus?.find((p) => p.id === profile.id)?.available;
|
||||||
|
|
||||||
|
return count || 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateRequested(neu, profile) {
|
||||||
|
if (neu === null || neu === '') return;
|
||||||
|
const newValue = Number(neu);
|
||||||
|
const availableCount = this.available(profile);
|
||||||
|
|
||||||
|
if (newValue < 0) {
|
||||||
|
profile.requested = 0;
|
||||||
|
} else if ( newValue > availableCount ) {
|
||||||
|
profile.requested = availableCount;
|
||||||
|
} else {
|
||||||
|
profile.requested = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:errors="errors"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
finish-button-mode="enable"
|
||||||
|
@finish="save"
|
||||||
|
@error="e=>errors=e"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
<Tabbed
|
||||||
|
v-bind="$attrs"
|
||||||
|
class="mt-15"
|
||||||
|
:side-tabs="true"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="profileSpec"
|
||||||
|
:label="t('harvester.migconfiguration.profileSpec')"
|
||||||
|
:weight="1"
|
||||||
|
class="bordered-table"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(profile, index) in profileSpec"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<LabelValue
|
||||||
|
:value="labelTitle(profile)"
|
||||||
|
class="mb-10"
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="profile.requested"
|
||||||
|
:min="0"
|
||||||
|
:disabled="isView"
|
||||||
|
type="number"
|
||||||
|
class="mb-20"
|
||||||
|
:label="`${t('harvester.migconfiguration.requested')}`"
|
||||||
|
@update:value="updateRequested($event, profile)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
@@ -8,7 +8,7 @@ import LabelValue from '@shell/components/LabelValue';
|
|||||||
import { BadgeState } from '@components/BadgeState';
|
import { BadgeState } from '@components/BadgeState';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { RadioGroup, RadioButton } from '@components/Form/Radio';
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
import HarvesterDisk from '../../mixins/harvester-disk';
|
import HarvesterDisk from '../../mixins/harvester-disk';
|
||||||
import Tags from '../../components/DiskTags';
|
import Tags from '../../components/DiskTags';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
@@ -30,7 +30,6 @@ export default {
|
|||||||
BadgeState,
|
BadgeState,
|
||||||
Banner,
|
Banner,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
RadioButton,
|
|
||||||
ModalWithCard,
|
ModalWithCard,
|
||||||
Tags,
|
Tags,
|
||||||
},
|
},
|
||||||
@@ -184,12 +183,11 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
forceFormattedDisabled() {
|
forceFormattedDisabled() {
|
||||||
const lastFormattedAt = this.blockDevice?.status?.deviceStatus?.fileSystem?.LastFormattedAt;
|
|
||||||
const fileSystem = this.blockDevice?.status?.deviceStatus?.fileSystem.type;
|
const fileSystem = this.blockDevice?.status?.deviceStatus?.fileSystem.type;
|
||||||
|
|
||||||
const systems = ['ext4', 'XFS'];
|
const systems = ['ext4', 'XFS'];
|
||||||
|
|
||||||
if (lastFormattedAt || this.blockDevice?.childParts?.length > 0) {
|
if (this.blockDevice?.childParts?.length > 0) {
|
||||||
return true;
|
return true;
|
||||||
} else if (systems.includes(fileSystem)) {
|
} else if (systems.includes(fileSystem)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -446,7 +444,7 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="(value.isNew && isLonghornV1 && !isFormatted) || isCorrupted"
|
v-if="(value.isNew && isLonghornV1) || isCorrupted"
|
||||||
class="col span-6"
|
class="col span-6"
|
||||||
>
|
>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
@@ -459,15 +457,6 @@ export default {
|
|||||||
:disabled="forceFormattedDisabled"
|
:disabled="forceFormattedDisabled"
|
||||||
tooltip-key="harvester.host.disk.forceFormatted.toolTip"
|
tooltip-key="harvester.host.disk.forceFormatted.toolTip"
|
||||||
>
|
>
|
||||||
<template #1="{option, listeners}">
|
|
||||||
<RadioButton
|
|
||||||
:label="option.label"
|
|
||||||
:val="option.value"
|
|
||||||
:value="value.forceFormatted"
|
|
||||||
:disabled="forceFormattedDisabled && !value.forceFormatted"
|
|
||||||
v-on="listeners"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
<script>
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import { DOC } from '../../config/doc-links';
|
||||||
|
|
||||||
|
export const hugepagesTHPEnabledMode = [{
|
||||||
|
label: 'Always',
|
||||||
|
value: 'always',
|
||||||
|
}, {
|
||||||
|
label: 'Madvise',
|
||||||
|
value: 'madvise',
|
||||||
|
}, {
|
||||||
|
label: 'Never',
|
||||||
|
value: 'never',
|
||||||
|
}];
|
||||||
|
|
||||||
|
export const hugepagesTHPShmemEnabledMode = [{
|
||||||
|
label: 'Always',
|
||||||
|
value: 'always',
|
||||||
|
}, {
|
||||||
|
label: 'Within Size',
|
||||||
|
value: 'within_size',
|
||||||
|
}, {
|
||||||
|
label: 'Advise',
|
||||||
|
value: 'advise',
|
||||||
|
}, {
|
||||||
|
label: 'Never',
|
||||||
|
value: 'never',
|
||||||
|
}, {
|
||||||
|
label: 'Deny',
|
||||||
|
value: 'deny',
|
||||||
|
}, {
|
||||||
|
label: 'Force',
|
||||||
|
value: 'force',
|
||||||
|
}];
|
||||||
|
|
||||||
|
export const hugepagesTHPDefragMode = [{
|
||||||
|
label: 'Always',
|
||||||
|
value: 'always',
|
||||||
|
}, {
|
||||||
|
label: 'Defer',
|
||||||
|
value: 'defer',
|
||||||
|
}, {
|
||||||
|
label: 'Defer+Madvise',
|
||||||
|
value: 'defer+madvise',
|
||||||
|
}, {
|
||||||
|
label: 'Madvise',
|
||||||
|
value: 'madvise',
|
||||||
|
}, {
|
||||||
|
label: 'Never',
|
||||||
|
value: 'never'
|
||||||
|
}];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterHugepages',
|
||||||
|
components: { LabeledSelect },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
node: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
docsTransparentHugepagesLink() {
|
||||||
|
return DOC.TRANSPARENT_HUGEPAGES;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async saveHugepages() {
|
||||||
|
this.hugepages['spec'] = this.spec;
|
||||||
|
|
||||||
|
await this.hugepages.save().catch((reason) => {
|
||||||
|
if (reason?.type === 'error') {
|
||||||
|
this.$store.dispatch('growl/error', {
|
||||||
|
title: this.t('harvester.notification.title.error'),
|
||||||
|
message: reason?.message,
|
||||||
|
}, { root: true });
|
||||||
|
|
||||||
|
return Promise.reject(new Error('saveHugepages error'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
|
||||||
|
|
||||||
|
this.hugepages = hash.find((node) => {
|
||||||
|
return node.id === this.node.id;
|
||||||
|
});
|
||||||
|
this.spec = this.hugepages.spec;
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hugepages: {},
|
||||||
|
spec: { transparent: {} },
|
||||||
|
hugepagesTHPEnabledMode,
|
||||||
|
hugepagesTHPShmemEnabledMode,
|
||||||
|
hugepagesTHPDefragMode,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.registerBeforeHook(this.saveHugepages, 'saveHugepages');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<hr class="divider" />
|
||||||
|
<h3>
|
||||||
|
<t
|
||||||
|
k="harvester.host.hugepages.transparent.title"
|
||||||
|
:raw="true"
|
||||||
|
:url="docsTransparentHugepagesLink"
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="spec.transparent.enabled"
|
||||||
|
:label="t('harvester.host.hugepages.transparent.enabled')"
|
||||||
|
:options="hugepagesTHPEnabledMode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="spec.transparent.shmemEnabled"
|
||||||
|
:label="t('harvester.host.hugepages.transparent.shmemEnabled')"
|
||||||
|
:options="hugepagesTHPShmemEnabledMode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="spec.transparent.defrag"
|
||||||
|
:label="t('harvester.host.hugepages.transparent.defrag')"
|
||||||
|
:options="hugepagesTHPDefragMode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -7,6 +7,8 @@ import { Checkbox } from '@components/Form/Checkbox';
|
|||||||
import { HCI } from '../../types';
|
import { 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 isEqual from 'lodash/isEqual';
|
||||||
|
import { clone } from '@shell/utils/object';
|
||||||
|
|
||||||
export const ksmtunedMode = [{
|
export const ksmtunedMode = [{
|
||||||
value: 'standard',
|
value: 'standard',
|
||||||
@@ -64,11 +66,13 @@ export default {
|
|||||||
|
|
||||||
this.enableMergeAcrossNodes = !!this.ksmtuned.spec?.mergeAcrossNodes;
|
this.enableMergeAcrossNodes = !!this.ksmtuned.spec?.mergeAcrossNodes;
|
||||||
this.spec = this.ksmtuned.spec;
|
this.spec = this.ksmtuned.spec;
|
||||||
|
this.originSpec = clone(this.ksmtuned.spec);
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
ksmtuned: {},
|
ksmtuned: {},
|
||||||
|
originSpec: {},
|
||||||
spec: {},
|
spec: {},
|
||||||
thresCoef: 30,
|
thresCoef: 30,
|
||||||
ksmtunedMode,
|
ksmtunedMode,
|
||||||
@@ -99,6 +103,11 @@ export default {
|
|||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async saveKsmtuned() {
|
async saveKsmtuned() {
|
||||||
|
// no ksmtuned spec change, skip save
|
||||||
|
if (isEqual(this.spec, this.originSpec)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.spec.mergeAcrossNodes = this.enableMergeAcrossNodes ? 1 : 0;
|
this.spec.mergeAcrossNodes = this.enableMergeAcrossNodes ? 1 : 0;
|
||||||
this.ksmtuned['spec'] = this.spec;
|
this.ksmtuned['spec'] = this.spec;
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { HCI } from '../../types';
|
|||||||
import HarvesterDisk from './HarvesterDisk';
|
import HarvesterDisk from './HarvesterDisk';
|
||||||
import HarvesterSeeder from './HarvesterSeeder';
|
import HarvesterSeeder from './HarvesterSeeder';
|
||||||
import HarvesterKsmtuned from './HarvesterKsmtuned';
|
import HarvesterKsmtuned from './HarvesterKsmtuned';
|
||||||
|
import HarvesterHugepages from './HarvesterHugepages';
|
||||||
import Tags from '../../components/DiskTags';
|
import Tags from '../../components/DiskTags';
|
||||||
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
@@ -50,6 +51,7 @@ export default {
|
|||||||
ArrayListGrouped,
|
ArrayListGrouped,
|
||||||
HarvesterDisk,
|
HarvesterDisk,
|
||||||
HarvesterKsmtuned,
|
HarvesterKsmtuned,
|
||||||
|
HarvesterHugepages,
|
||||||
ButtonDropdown,
|
ButtonDropdown,
|
||||||
KeyValue,
|
KeyValue,
|
||||||
Banner,
|
Banner,
|
||||||
@@ -225,6 +227,12 @@ export default {
|
|||||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hasHugepagesSchema() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.HUGEPAGES);
|
||||||
|
},
|
||||||
|
|
||||||
hasBlockDevicesSchema() {
|
hasBlockDevicesSchema() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
@@ -527,7 +535,6 @@ export default {
|
|||||||
id: this.longhornNode.id,
|
id: this.longhornNode.id,
|
||||||
opt: { force: true },
|
opt: { force: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, '5000'));
|
await new Promise((resolve) => setTimeout(resolve, '5000'));
|
||||||
await retrySave();
|
await retrySave();
|
||||||
} else {
|
} else {
|
||||||
@@ -648,6 +655,17 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
</ArrayListGrouped>
|
</ArrayListGrouped>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
v-if="hasHugepagesSchema"
|
||||||
|
name="Hugepages"
|
||||||
|
:weight="70"
|
||||||
|
:label="t('harvester.host.tabs.hugepages')"
|
||||||
|
>
|
||||||
|
<HarvesterHugepages
|
||||||
|
:node="value"
|
||||||
|
:register-before-hook="registerBeforeHook"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
v-if="hasKsmtunedSchema"
|
v-if="hasKsmtunedSchema"
|
||||||
name="Ksmtuned"
|
name="Ksmtuned"
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-anno
|
|||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import 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';
|
import { NETWORK_TYPE, L2VLAN_MODE } from '../config/types';
|
||||||
|
import { removeObject } from '@shell/utils/array';
|
||||||
|
|
||||||
const { L2VLAN, UNTAGGED, OVERLAY } = NETWORK_TYPE;
|
const {
|
||||||
|
L2VLAN, UNTAGGED, OVERLAY, L2TRUNK_VLAN
|
||||||
|
} = NETWORK_TYPE;
|
||||||
|
const { ACCESS, TRUNK } = L2VLAN_MODE;
|
||||||
|
|
||||||
const AUTO = 'auto';
|
const AUTO = 'auto';
|
||||||
const MANUAL = 'manual';
|
const MANUAL = 'manual';
|
||||||
@@ -52,6 +56,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
config,
|
config,
|
||||||
type,
|
type,
|
||||||
|
l2VlanMode: this.value.vlanType === L2TRUNK_VLAN ? TRUNK : ACCESS,
|
||||||
|
vlanTrunk: this.parseVlanTrunk(config),
|
||||||
layer3Network: {
|
layer3Network: {
|
||||||
mode: layer3Network.mode || AUTO,
|
mode: layer3Network.mode || AUTO,
|
||||||
serverIPAddr: layer3Network.serverIPAddr || '',
|
serverIPAddr: layer3Network.serverIPAddr || '',
|
||||||
@@ -108,6 +114,10 @@ export default {
|
|||||||
}];
|
}];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
l2VlanTrunkModeFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('l2VlanTrunkMode');
|
||||||
|
},
|
||||||
|
|
||||||
kubeovnVpcSubnetSupport() {
|
kubeovnVpcSubnetSupport() {
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('kubeovnVpcSubnet');
|
return this.$store.getters['harvester-common/getFeatureEnabled']('kubeovnVpcSubnet');
|
||||||
},
|
},
|
||||||
@@ -131,6 +141,16 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
l2VlanModeOptions() {
|
||||||
|
return [{
|
||||||
|
label: this.t('harvester.vlanStatus.vlanConfig.l2VlanMode.access'),
|
||||||
|
value: ACCESS,
|
||||||
|
}, {
|
||||||
|
label: this.t('harvester.vlanStatus.vlanConfig.l2VlanMode.trunk'),
|
||||||
|
value: TRUNK,
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
|
||||||
networkTypes() {
|
networkTypes() {
|
||||||
const types = [L2VLAN, UNTAGGED];
|
const types = [L2VLAN, UNTAGGED];
|
||||||
|
|
||||||
@@ -149,6 +169,22 @@ export default {
|
|||||||
return this.type === L2VLAN;
|
return this.type === L2VLAN;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isL2VlanTrunkMode() {
|
||||||
|
if (this.isView) {
|
||||||
|
return this.value.vlanType === L2TRUNK_VLAN && this.l2VlanMode === TRUNK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.type === L2TRUNK_VLAN || (this.l2VlanMode === TRUNK && this.type === L2VLAN);
|
||||||
|
},
|
||||||
|
|
||||||
|
isL2VlanAccessMode() {
|
||||||
|
if (this.isView) {
|
||||||
|
return this.value.vlanType === L2VLAN && this.l2VlanMode === ACCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.type === L2VLAN && this.l2VlanMode === ACCESS;
|
||||||
|
},
|
||||||
|
|
||||||
isOverlayNetwork() {
|
isOverlayNetwork() {
|
||||||
if (this.isView) {
|
if (this.isView) {
|
||||||
return this.value.vlanType === OVERLAY;
|
return this.value.vlanType === OVERLAY;
|
||||||
@@ -168,11 +204,11 @@ export default {
|
|||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
type(newType) {
|
type(newType) {
|
||||||
if (newType === OVERLAY) {
|
if (newType === OVERLAY) { // overlay network configuration
|
||||||
this.config.type = 'kube-ovn';
|
this.config.type = 'kube-ovn';
|
||||||
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
|
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
|
||||||
this.config.server_socket = '/run/openvswitch/kube-ovn-daemon.sock';
|
this.config.server_socket = '/run/openvswitch/kube-ovn-daemon.sock';
|
||||||
} else {
|
} else { // l2vlan or untagged network configuration
|
||||||
this.config.type = 'bridge';
|
this.config.type = 'bridge';
|
||||||
this.config.promiscMode = true;
|
this.config.promiscMode = true;
|
||||||
this.config.ipam = {};
|
this.config.ipam = {};
|
||||||
@@ -180,7 +216,20 @@ export default {
|
|||||||
delete this.config.provider;
|
delete this.config.provider;
|
||||||
delete this.config.server_socket;
|
delete this.config.server_socket;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
l2VlanMode(newAccessMode) {
|
||||||
|
if (this.type !== L2VLAN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newAccessMode === TRUNK) { // trunk mode
|
||||||
|
this.config.vlan = 0;
|
||||||
|
this.config.vlanTrunk = this.vlanTrunk;
|
||||||
|
} else { // access mode
|
||||||
|
delete this.config.vlanTrunk;
|
||||||
|
this.config.vlan = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -188,7 +237,15 @@ export default {
|
|||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
if (this.isL2VlanNetwork || this.isUntaggedNetwork) {
|
if (this.isL2VlanNetwork || this.isUntaggedNetwork) {
|
||||||
if (!this.config.vlan && !this.isUntaggedNetwork) {
|
if (this.isL2VlanTrunkMode && this.vlanTrunk.some((trunk) => trunk.minID === '')) {
|
||||||
|
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.vlanStatus.vlanConfig.vlanTrunk.minId') }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isL2VlanTrunkMode && this.vlanTrunk.some((trunk) => trunk.maxID === '')) {
|
||||||
|
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.vlanStatus.vlanConfig.vlanTrunk.maxId') }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isL2VlanAccessMode && !this.config.vlan && !this.isUntaggedNetwork) {
|
||||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
|
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,6 +274,30 @@ export default {
|
|||||||
await this.save(buttonCb);
|
await this.save(buttonCb);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
parseVlanTrunk(config) {
|
||||||
|
if (config?.vlanTrunk && config?.vlanTrunk?.length > 0) {
|
||||||
|
return config.vlanTrunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{ minID: '', maxID: '' }];
|
||||||
|
},
|
||||||
|
|
||||||
|
removeVlanTrunk(trunk) {
|
||||||
|
removeObject(this.vlanTrunk, trunk);
|
||||||
|
},
|
||||||
|
|
||||||
|
addVlanTrunk() {
|
||||||
|
if (!this.config.vlanTrunk) {
|
||||||
|
this.config.vlanTrunk = [];
|
||||||
|
}
|
||||||
|
this.vlanTrunk.push({ minID: this.minId, maxID: this.maxId });
|
||||||
|
this.config.vlanTrunk = this.vlanTrunk;
|
||||||
|
},
|
||||||
|
|
||||||
|
vlanTrunkChange() {
|
||||||
|
this.config.vlanTrunk = this.vlanTrunk;
|
||||||
|
},
|
||||||
|
|
||||||
input(neu) {
|
input(neu) {
|
||||||
if (neu === '') {
|
if (neu === '') {
|
||||||
this.config.vlan = '';
|
this.config.vlan = '';
|
||||||
@@ -292,8 +373,76 @@ export default {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-if="isL2VlanNetwork && l2VlanTrunkModeFeatureEnabled"
|
||||||
|
v-model:value="l2VlanMode"
|
||||||
|
class="mb-20"
|
||||||
|
:options="l2VlanModeOptions"
|
||||||
|
:mode="mode"
|
||||||
|
:disabled="isEdit"
|
||||||
|
:label="t('harvester.vlanStatus.vlanConfig.l2VlanMode.label')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div v-if="isL2VlanTrunkMode && l2VlanTrunkModeFeatureEnabled">
|
||||||
|
<div
|
||||||
|
v-for="(trunk, i) in vlanTrunk"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col trunk-span">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value.number="trunk.minID"
|
||||||
|
class="mb-20"
|
||||||
|
required
|
||||||
|
placeholder="e.g. 1-4094"
|
||||||
|
:min="1"
|
||||||
|
:max="4094"
|
||||||
|
type="number"
|
||||||
|
:label="t('harvester.vlanStatus.vlanConfig.vlanTrunk.minId')"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="vlanTrunkChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col trunk-span">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value.number="trunk.maxID"
|
||||||
|
class="mb-20"
|
||||||
|
:max="4094"
|
||||||
|
:min="1"
|
||||||
|
placeholder="e.g. 1-4094"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
:label="t('harvester.vlanStatus.vlanConfig.vlanTrunk.maxId')"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="vlanTrunkChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col remove-btn mb-20">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:disabled="isView || vlanTrunk.length <= 1"
|
||||||
|
:aria-label="t('generic.ariaLabel.remove', {index: i+1})"
|
||||||
|
role="button"
|
||||||
|
class="btn role-link"
|
||||||
|
@click="removeVlanTrunk(trunk)"
|
||||||
|
>
|
||||||
|
{{ t('harvester.fields.remove') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="isL2VlanTrunkMode"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm bg-primary mb-20"
|
||||||
|
:disabled="isView"
|
||||||
|
@click="addVlanTrunk"
|
||||||
|
>
|
||||||
|
{{ t('harvester.vlanStatus.vlanConfig.vlanTrunk.add') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-if="isL2VlanNetwork"
|
v-if="isL2VlanAccessMode"
|
||||||
v-model:value.number="config.vlan"
|
v-model:value.number="config.vlan"
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
required
|
required
|
||||||
@@ -316,7 +465,7 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
v-if="isL2VlanNetwork"
|
v-if="isL2VlanAccessMode"
|
||||||
name="layer3Network"
|
name="layer3Network"
|
||||||
:label="t('harvester.network.tabs.layer3Network')"
|
:label="t('harvester.network.tabs.layer3Network')"
|
||||||
:weight="98"
|
:weight="98"
|
||||||
@@ -375,3 +524,12 @@ export default {
|
|||||||
</Tabbed>
|
</Tabbed>
|
||||||
</CruResource>
|
</CruResource>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.remove-btn {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
.trunk-span{
|
||||||
|
flex: 5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { HCI } from '../types';
|
|||||||
import { sortBy } from '@shell/utils/sort';
|
import { sortBy } from '@shell/utils/sort';
|
||||||
import { BACKUP_TYPE } from '../config/types';
|
import { BACKUP_TYPE } from '../config/types';
|
||||||
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
||||||
|
import { isBackupTargetSettingEmpty, isBackupTargetSettingUnavailable } from '../utils/setting';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CreateVMSchedule',
|
name: 'CreateVMSchedule',
|
||||||
@@ -93,7 +94,7 @@ export default {
|
|||||||
return this.settings.find( (O) => O.id === 'backup-target');
|
return this.settings.find( (O) => O.id === 'backup-target');
|
||||||
},
|
},
|
||||||
isEmptyValue() {
|
isEmptyValue() {
|
||||||
return this.getBackupTargetValueIsEmpty(this.backupTargetResource);
|
return isBackupTargetSettingEmpty(this.backupTargetResource);
|
||||||
},
|
},
|
||||||
canUpdate() {
|
canUpdate() {
|
||||||
return this?.backupTargetResource?.canUpdate;
|
return this?.backupTargetResource?.canUpdate;
|
||||||
@@ -109,7 +110,7 @@ export default {
|
|||||||
!!this.value.spec.maxFailure;
|
!!this.value.spec.maxFailure;
|
||||||
},
|
},
|
||||||
isBackupTargetUnAvailable() {
|
isBackupTargetUnAvailable() {
|
||||||
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && (this.errorMessage || this.isEmptyValue) && this.canUpdate;
|
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && isBackupTargetSettingUnavailable(this.backupTargetResource);
|
||||||
},
|
},
|
||||||
vmOptions() {
|
vmOptions() {
|
||||||
const nsVmList = this.$store.getters['harvester/all'](HCI.VM).filter((vm) => vm.metadata.namespace === this.value.metadata.namespace);
|
const nsVmList = this.$store.getters['harvester/all'](HCI.VM).filter((vm) => vm.metadata.namespace === this.value.metadata.namespace);
|
||||||
@@ -166,20 +167,6 @@ export default {
|
|||||||
onTypeChange(newType) {
|
onTypeChange(newType) {
|
||||||
this.value.metadata.name = `svm${ newType }-${ this.value.spec.vmbackup.source.name }`;
|
this.value.metadata.name = `svm${ newType }-${ this.value.spec.vmbackup.source.name }`;
|
||||||
},
|
},
|
||||||
getBackupTargetValueIsEmpty(resource) {
|
|
||||||
let out = true;
|
|
||||||
|
|
||||||
if (resource?.value) {
|
|
||||||
try {
|
|
||||||
const valueJson = JSON.parse(resource?.value);
|
|
||||||
|
|
||||||
out = !valueJson.type;
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
|
||||||
},
|
|
||||||
|
|
||||||
validateFailure(count) {
|
validateFailure(count) {
|
||||||
if (this.value.spec.retain && count > this.value.spec.retain) {
|
if (this.value.spec.retain && count > this.value.spec.retain) {
|
||||||
this.value.spec['maxFailure'] = this.value.spec.retain;
|
this.value.spec['maxFailure'] = this.value.spec.retain;
|
||||||
|
|||||||
@@ -296,4 +296,9 @@ export default {
|
|||||||
:deep() .edit-help code {
|
:deep() .edit-help code {
|
||||||
padding: 1px 5px;
|
padding: 1px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep(.banner__content.closable) {
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,319 @@
|
|||||||
|
<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,4 +1,5 @@
|
|||||||
<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';
|
||||||
@@ -6,11 +7,9 @@ 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 {
|
||||||
@@ -22,6 +21,7 @@ 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';
|
||||||
|
|
||||||
export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node';
|
export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node';
|
||||||
|
|
||||||
@@ -45,6 +45,7 @@ export default {
|
|||||||
Tabbed,
|
Tabbed,
|
||||||
Loading,
|
Loading,
|
||||||
Tags,
|
Tags,
|
||||||
|
CDISettings,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
@@ -117,6 +118,7 @@ export default {
|
|||||||
key: '',
|
key: '',
|
||||||
values: [],
|
values: [],
|
||||||
},
|
},
|
||||||
|
cdiSettings: {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -124,10 +126,10 @@ export default {
|
|||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
const hash = {
|
const hash = {
|
||||||
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) {
|
||||||
@@ -197,6 +199,14 @@ 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: {
|
||||||
@@ -249,6 +259,10 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.formatAllowedTopoloties();
|
this.formatAllowedTopoloties();
|
||||||
|
|
||||||
|
if (this.shouldShowCDISettingsTab) {
|
||||||
|
this.formatCDISettings();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
formatAllowedTopoloties() {
|
formatAllowedTopoloties() {
|
||||||
@@ -270,6 +284,42 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -404,6 +454,19 @@ 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>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ 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';
|
||||||
@@ -168,11 +169,18 @@ export default {
|
|||||||
|
|
||||||
return filteredStorages
|
return filteredStorages
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
let 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
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -257,8 +257,12 @@ 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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import ResourceTabs from '@shell/components/form/ResourceTabs';
|
|||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import Conditions from '@shell/components/form/Conditions';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { get } from '@shell/utils/object';
|
import { get } from '@shell/utils/object';
|
||||||
@@ -23,6 +24,7 @@ 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 { GIBIBYTE } from '../utils/unit';
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||||
|
import { isInternalStorageClass } from '../utils/storage-class';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterVolume',
|
name: 'HarvesterVolume',
|
||||||
@@ -39,6 +41,7 @@ export default {
|
|||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
LabeledInput,
|
LabeledInput,
|
||||||
NameNsDescription,
|
NameNsDescription,
|
||||||
|
Conditions
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
@@ -206,11 +209,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) => {
|
||||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
let 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
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
},
|
},
|
||||||
@@ -259,7 +269,19 @@ export default {
|
|||||||
|
|
||||||
rebuildStatus() {
|
rebuildStatus() {
|
||||||
return this.value.longhornEngine?.status?.rebuildStatus;
|
return this.value.longhornEngine?.status?.rebuildStatus;
|
||||||
}
|
},
|
||||||
|
|
||||||
|
isLHV2VolExpansionFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
|
||||||
|
},
|
||||||
|
|
||||||
|
isResizeDisabled() {
|
||||||
|
return (
|
||||||
|
!this.isLHV2VolExpansionFeatureEnabled &&
|
||||||
|
this.value?.isLonghornV2 &&
|
||||||
|
this.isEdit
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@@ -455,7 +477,7 @@ export default {
|
|||||||
:output-modifier="true"
|
:output-modifier="true"
|
||||||
:increment="1024"
|
:increment="1024"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:disabled="value?.isLonghornV2 && isEdit"
|
:disabled="isResizeDisabled"
|
||||||
required
|
required
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
:suffix="GIBIBYTE"
|
:suffix="GIBIBYTE"
|
||||||
@@ -463,7 +485,7 @@ export default {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Banner
|
<Banner
|
||||||
v-if="value?.isLonghornV2 && isEdit"
|
v-if="isResizeDisabled"
|
||||||
color="warning"
|
color="warning"
|
||||||
>
|
>
|
||||||
<span>{{ t('harvester.volume.longhorn.disableResize') }}</span>
|
<span>{{ t('harvester.volume.longhorn.disableResize') }}</span>
|
||||||
@@ -573,6 +595,15 @@ export default {
|
|||||||
:label="t('nameNsDescription.name.label')"
|
:label="t('nameNsDescription.name.label')"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
v-if="!isCreate"
|
||||||
|
name="conditions"
|
||||||
|
:label="t('harvester.volume.tabs.conditions')"
|
||||||
|
class="bordered-table"
|
||||||
|
:mode="mode"
|
||||||
|
>
|
||||||
|
<Conditions :value="value" />
|
||||||
|
</Tab>
|
||||||
</ResourceTabs>
|
</ResourceTabs>
|
||||||
</CruResource>
|
</CruResource>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
<script>
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
||||||
|
import { removeAt } from '@shell/utils/array';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import InfoBox from '@shell/components/InfoBox';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AccessControlList',
|
||||||
|
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Banner, InfoBox, LabeledSelect, LabeledInput
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const rows = (this.value || []).map((row) => {
|
||||||
|
return {
|
||||||
|
action: row.action || '',
|
||||||
|
direction: row.direction || '',
|
||||||
|
priority: row.priority || 0,
|
||||||
|
match: row.match || '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { rows };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isView() {
|
||||||
|
return this.mode === _VIEW;
|
||||||
|
},
|
||||||
|
|
||||||
|
actionOptions() {
|
||||||
|
return [
|
||||||
|
{ label: 'Allow', value: 'allow' },
|
||||||
|
{ label: 'Drop', value: 'drop' },
|
||||||
|
{ label: 'Pass', value: 'pass' },
|
||||||
|
{ label: 'Reject', value: 'reject' },
|
||||||
|
{ label: 'Allow-related', value: 'allow-related' },
|
||||||
|
{ label: 'Allow-stateless', value: 'allow-stateless' },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
directionOptions() {
|
||||||
|
return [
|
||||||
|
{ label: 'To-lport', value: 'to-lport' },
|
||||||
|
{ label: 'From-lport', value: 'from-lport' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.queueUpdate = debounce(this.update, 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
add() {
|
||||||
|
this.rows.push({
|
||||||
|
action: '',
|
||||||
|
direction: '',
|
||||||
|
priority: this.rows.length,
|
||||||
|
match: '',
|
||||||
|
});
|
||||||
|
this.queueUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
|
removeRule(idx) {
|
||||||
|
removeAt(this.rows, idx);
|
||||||
|
this.queueUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (this.isView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('update:value', this.rows);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Banner
|
||||||
|
v-if="rows.length > 0"
|
||||||
|
color="info"
|
||||||
|
>
|
||||||
|
<t
|
||||||
|
k="harvester.subnet.acl.banner"
|
||||||
|
:raw="true"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
<div
|
||||||
|
v-for="(row, idx) in rows"
|
||||||
|
:key="idx"
|
||||||
|
>
|
||||||
|
<InfoBox class="box">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="role-link btn btn-sm removeBtn"
|
||||||
|
:disabled="isView"
|
||||||
|
@click="removeRule(idx)"
|
||||||
|
>
|
||||||
|
<i class="icon icon-x" />
|
||||||
|
</button>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="row.action"
|
||||||
|
:mode="mode"
|
||||||
|
class="mt-5 ml-5"
|
||||||
|
:options="actionOptions"
|
||||||
|
:required="true"
|
||||||
|
:label="t('harvester.subnet.acl.action.label')"
|
||||||
|
:placeholder="t('harvester.subnet.acl.action.placeholder')"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="row.direction"
|
||||||
|
:mode="mode"
|
||||||
|
:options="directionOptions"
|
||||||
|
class="mt-5"
|
||||||
|
:required="true"
|
||||||
|
:label="t('harvester.subnet.acl.direction.label')"
|
||||||
|
:placeholder="t('harvester.subnet.acl.direction.placeholder')"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value.number="row.priority"
|
||||||
|
type="number"
|
||||||
|
class="mb-20 mt-5"
|
||||||
|
:max="32767"
|
||||||
|
:min="0"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
label-key="harvester.subnet.acl.priority.label"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-11">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="row.match"
|
||||||
|
class="mb-5 ml-5"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
:placeholder="t('harvester.subnet.acl.match.placeholder')"
|
||||||
|
label-key="harvester.subnet.acl.match.label"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</InfoBox>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn role-tertiary add"
|
||||||
|
:disabled="isView"
|
||||||
|
@click="add()"
|
||||||
|
>
|
||||||
|
<t k="harvester.subnet.acl.addRule" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.box {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.removeBtn {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -14,6 +14,8 @@ import ArrayList from '@shell/components/form/ArrayList';
|
|||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
import ResourceTabs from '@shell/components/form/ResourceTabs/index';
|
import ResourceTabs from '@shell/components/form/ResourceTabs/index';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AccessControlList from './AccessControlList';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'EditSubnet',
|
name: 'EditSubnet',
|
||||||
@@ -21,6 +23,7 @@ export default {
|
|||||||
emits: ['update:value'],
|
emits: ['update:value'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
|
Banner,
|
||||||
CruResource,
|
CruResource,
|
||||||
LabeledInput,
|
LabeledInput,
|
||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
@@ -30,6 +33,7 @@ export default {
|
|||||||
ArrayList,
|
ArrayList,
|
||||||
ResourceTabs,
|
ResourceTabs,
|
||||||
Loading,
|
Loading,
|
||||||
|
AccessControlList
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
@@ -38,7 +42,9 @@ export default {
|
|||||||
|
|
||||||
created() {
|
created() {
|
||||||
const vpc = this.$route.query.vpc || '';
|
const vpc = this.$route.query.vpc || '';
|
||||||
|
const enableDHCP = this.value?.spec?.enableDHCP || false;
|
||||||
|
|
||||||
|
set(this.value.spec, 'enableDHCP', enableDHCP);
|
||||||
set(this.value, 'spec', this.value.spec || {
|
set(this.value, 'spec', this.value.spec || {
|
||||||
cidrBlock: '',
|
cidrBlock: '',
|
||||||
protocol: NETWORK_PROTOCOL.IPv4,
|
protocol: NETWORK_PROTOCOL.IPv4,
|
||||||
@@ -46,7 +52,9 @@ export default {
|
|||||||
vpc,
|
vpc,
|
||||||
gatewayIP: '',
|
gatewayIP: '',
|
||||||
excludeIps: [],
|
excludeIps: [],
|
||||||
private: false
|
private: false,
|
||||||
|
enableDHCP,
|
||||||
|
acls: []
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -81,7 +89,6 @@ export default {
|
|||||||
protocolOptions() {
|
protocolOptions() {
|
||||||
return Object.values(NETWORK_PROTOCOL);
|
return Object.values(NETWORK_PROTOCOL);
|
||||||
},
|
},
|
||||||
|
|
||||||
provider: {
|
provider: {
|
||||||
get() {
|
get() {
|
||||||
const raw = this.value.spec.provider;
|
const raw = this.value.spec.provider;
|
||||||
@@ -125,10 +132,21 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'value.spec.enableDHCP': {
|
||||||
|
handler(newValue) {
|
||||||
|
if (newValue === false) {
|
||||||
|
this.value.spec.dhcpV4Options = '';
|
||||||
|
this.value.spec.dhcpV6Options = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async saveSubnet(buttonCb) {
|
async saveSubnet(buttonCb) {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
const name = this.value?.metadata?.name;
|
const name = this.value?.metadata?.name;
|
||||||
|
const hasEmptyAcls = this.value?.spec?.acls?.some((acl) => !acl.match || !acl.action || acl.priority === undefined || acl.priority === null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@@ -139,6 +157,8 @@ export default {
|
|||||||
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.provider.label') }, true));
|
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.provider.label') }, true));
|
||||||
} else if (this.value.spec.excludeIps.includes('')) {
|
} else if (this.value.spec.excludeIps.includes('')) {
|
||||||
errors.push(this.t('harvester.validation.subnet.excludeIps'));
|
errors.push(this.t('harvester.validation.subnet.excludeIps'));
|
||||||
|
} else if (hasEmptyAcls) {
|
||||||
|
errors.push(this.t('harvester.validation.subnet.aclEmptyError'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
@@ -245,6 +265,45 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="value.spec.enableDHCP"
|
||||||
|
name="enabled"
|
||||||
|
:options="[true, false]"
|
||||||
|
:label="t('harvester.subnet.dhcp.label')"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
:mode="mode"
|
||||||
|
:tooltip="t('harvester.subnet.dhcp.tooltip')"
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-if="value.spec.enableDHCP && value.spec.protocol === 'IPv4'"
|
||||||
|
v-model:value="value.spec.dhcpV4Options"
|
||||||
|
class="mb-20 mt-20"
|
||||||
|
:placeholder="t('harvester.subnet.dhcp.placeholder')"
|
||||||
|
:label="t('harvester.subnet.dhcp.v4Options')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-if="value.spec.enableDHCP && value.spec.protocol === 'IPv6'"
|
||||||
|
v-model:value="value.spec.dhcpV6Options"
|
||||||
|
:placeholder="t('harvester.subnet.dhcp.placeholder')"
|
||||||
|
class="mb-20 mt-20"
|
||||||
|
:label="t('harvester.subnet.dhcp.v6Options')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
v-if="value.spec.enableDHCP"
|
||||||
|
color="info"
|
||||||
|
class="dhcpOption-banner"
|
||||||
|
>
|
||||||
|
<t
|
||||||
|
k="harvester.subnet.dhcp.dhcpOptionBanner"
|
||||||
|
:raw="true"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row mt-20">
|
<div class="row mt-20">
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
@@ -312,12 +371,29 @@ export default {
|
|||||||
<div class="key">
|
<div class="key">
|
||||||
<input
|
<input
|
||||||
v-model="scope.row.value"
|
v-model="scope.row.value"
|
||||||
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
:placeholder="t('harvester.subnet.excludeIPs.placeholder')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ArrayList>
|
</ArrayList>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
name="ACL"
|
||||||
|
:label="t('harvester.subnet.acl.label')"
|
||||||
|
:weight="-2"
|
||||||
|
class="bordered-table"
|
||||||
|
>
|
||||||
|
<AccessControlList
|
||||||
|
v-model:value="value.spec.acls"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
</ResourceTabs>
|
</ResourceTabs>
|
||||||
</CruResource>
|
</CruResource>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.dhcpOption-banner {
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -2,12 +2,17 @@
|
|||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
||||||
import { removeAt } from '@shell/utils/array';
|
import { removeAt } from '@shell/utils/array';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { DOC } from '../../config/doc-links';
|
||||||
|
import { docLink } from '../../utils/feature-flags';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'StaticRoutes',
|
name: 'StaticRoutes',
|
||||||
|
|
||||||
emits: ['update:value'],
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: { Banner },
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@@ -45,6 +50,12 @@ export default {
|
|||||||
nextHopIPTooltip() {
|
nextHopIPTooltip() {
|
||||||
return this.t('harvester.vpc.staticRoutes.nextHopIP.tooltip');
|
return this.t('harvester.vpc.staticRoutes.nextHopIP.tooltip');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
vpcPeeringExamplesLink() {
|
||||||
|
const version = this.$store.getters['harvester-common/getServerVersion']();
|
||||||
|
|
||||||
|
return docLink(DOC.VPC_CONFIGURATION_EXAMPLES, version);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
@@ -78,6 +89,13 @@ export default {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<Banner color="info">
|
||||||
|
<t
|
||||||
|
k="harvester.vpc.vpcPeerings.infoBanner"
|
||||||
|
:raw="true"
|
||||||
|
:url="vpcPeeringExamplesLink"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
<div
|
<div
|
||||||
v-if="rows.length"
|
v-if="rows.length"
|
||||||
class="static-route-row"
|
class="static-route-row"
|
||||||
|
|||||||
@@ -2,23 +2,44 @@
|
|||||||
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 { 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: { UnitInput, InputOrDisplay },
|
components: {
|
||||||
|
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',
|
||||||
@@ -29,21 +50,59 @@ 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,
|
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: {
|
||||||
cupDisplay() {
|
isView() {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -55,71 +114,161 @@ 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: {
|
||||||
change() {
|
hotPlugChanged(neu) {
|
||||||
let memory = '';
|
// If hot plug is enabled, we need to update the maxCpu and maxMemory values
|
||||||
|
if (neu) {
|
||||||
if (String(this.localMemory).includes('Gi')) {
|
this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null;
|
||||||
memory = this.localMemory;
|
this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null;
|
||||||
|
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, neu);
|
||||||
} else {
|
} else {
|
||||||
memory = `${ this.localMemory }${ GIBIBYTE }`;
|
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, '', null, neu);
|
||||||
}
|
}
|
||||||
if (memory.includes('null')) {
|
|
||||||
memory = null;
|
|
||||||
}
|
|
||||||
this.$emit('updateCpuMemory', this.localCpu, memory);
|
|
||||||
},
|
},
|
||||||
|
changeMemory() {
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
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 class="row">
|
<div>
|
||||||
<div class="col span-6 mb-10">
|
<div class="row">
|
||||||
<InputOrDisplay
|
<div class="col span-6 mb-10">
|
||||||
name="CPU"
|
<InputOrDisplay
|
||||||
:value="cupDisplay"
|
name="CPU"
|
||||||
:mode="mode"
|
:value="cpuDisplay"
|
||||||
>
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="localCpu"
|
|
||||||
label="CPU"
|
|
||||||
suffix="C"
|
|
||||||
:delay="0"
|
|
||||||
required
|
|
||||||
:disabled="disabled"
|
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
class="mb-20"
|
class="mb-10"
|
||||||
@update:value="change"
|
>
|
||||||
/>
|
<UnitInput
|
||||||
</InputOrDisplay>
|
v-model:value="localCpu"
|
||||||
|
label="CPU"
|
||||||
|
suffix="C"
|
||||||
|
:delay="0"
|
||||||
|
required
|
||||||
|
:disabled="disabled"
|
||||||
|
:mode="mode"
|
||||||
|
class="mb-20"
|
||||||
|
@update:value="changeCPU"
|
||||||
|
/>
|
||||||
|
</InputOrDisplay>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6 mb-10">
|
||||||
|
<InputOrDisplay
|
||||||
|
:name="t('harvester.virtualMachine.input.memory')"
|
||||||
|
:value="memoryDisplay"
|
||||||
|
:mode="mode"
|
||||||
|
>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="localMemory"
|
||||||
|
:label="t('harvester.virtualMachine.input.memory')"
|
||||||
|
:mode="mode"
|
||||||
|
:input-exponent="3"
|
||||||
|
:delay="0"
|
||||||
|
:increment="1024"
|
||||||
|
:output-modifier="true"
|
||||||
|
:disabled="disabled"
|
||||||
|
required
|
||||||
|
:suffix="GIBIBYTE"
|
||||||
|
class="mb-20"
|
||||||
|
@update:value="changeMemory"
|
||||||
|
/>
|
||||||
|
</InputOrDisplay>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
<div class="col span-6 mb-10">
|
v-if="isCPUMemoryHotPlugFeatureEnabled"
|
||||||
<InputOrDisplay
|
class="row"
|
||||||
:name="t('harvester.virtualMachine.input.memory')"
|
>
|
||||||
:value="memoryDisplay"
|
<Checkbox
|
||||||
:mode="mode"
|
v-model:value="localEnableHotPlug"
|
||||||
>
|
class="check"
|
||||||
<UnitInput
|
type="checkbox"
|
||||||
v-model:value="localMemory"
|
:label="t('harvester.virtualMachine.hotplug.title')"
|
||||||
:label="t('harvester.virtualMachine.input.memory')"
|
: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"
|
:mode="mode"
|
||||||
:input-exponent="3"
|
class="mt-20"
|
||||||
:delay="0"
|
>
|
||||||
:increment="1024"
|
<UnitInput
|
||||||
:output-modifier="true"
|
v-model:value="maxLocalCpu"
|
||||||
:disabled="disabled"
|
:label="t('harvester.virtualMachine.input.maxCpu')"
|
||||||
required
|
suffix="C"
|
||||||
:suffix="GIBIBYTE"
|
:delay="0"
|
||||||
class="mb-20"
|
:disabled="disabled"
|
||||||
@update:value="change"
|
:mode="mode"
|
||||||
/>
|
class="mt-20"
|
||||||
</InputOrDisplay>
|
@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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import InfoBox from '@shell/components/InfoBox';
|
import InfoBox from '@shell/components/InfoBox';
|
||||||
|
|
||||||
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||||
import { sortBy } from '@shell/utils/sort';
|
import { sortBy } from '@shell/utils/sort';
|
||||||
import { clone } from '@shell/utils/object';
|
import { clone } from '@shell/utils/object';
|
||||||
@@ -14,6 +13,13 @@ export default {
|
|||||||
components: { InfoBox, Base },
|
components: { InfoBox, Base },
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
vm: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'create'
|
default: 'create'
|
||||||
@@ -32,10 +38,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
await this.fetchHotunplugData();
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
rows: this.addKeyId(clone(this.value)),
|
nameIdx: 1,
|
||||||
nameIdx: 1
|
rows: this.addKeyId(clone(this.value)),
|
||||||
|
hotunpluggableNics: new Set(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -64,15 +75,57 @@ export default {
|
|||||||
|
|
||||||
return out;
|
return out;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
canCheckHotunplug() {
|
||||||
|
return !!this.vm?.actions?.findHotunpluggableNics;
|
||||||
|
},
|
||||||
|
|
||||||
|
vmState() {
|
||||||
|
return this.vm?.stateDisplay;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
value(neu) {
|
value(neu) {
|
||||||
this.rows = neu;
|
this.rows = this.mergeHotplugData(clone(neu));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
vmState(newState, oldState) {
|
||||||
|
if (newState !== oldState) {
|
||||||
|
this.fetchHotunplugData();
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
async fetchHotunplugData() {
|
||||||
|
if (!this.canCheckHotunplug) {
|
||||||
|
this.rows = this.mergeHotplugData(clone(this.value));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await this.vm.doAction('findHotunpluggableNics');
|
||||||
|
|
||||||
|
this.hotunpluggableNics = new Set(resp?.interfaces || []);
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Failed to fetch hot-unpluggable NICs:', e);
|
||||||
|
this.hotunpluggableNics = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rows = this.mergeHotplugData(clone(this.value));
|
||||||
|
},
|
||||||
|
|
||||||
|
mergeHotplugData(networks) {
|
||||||
|
return (networks || []).map((network) => ({
|
||||||
|
...network,
|
||||||
|
isHotunpluggable: this.hotunpluggableNics.has(network.name),
|
||||||
|
rowKeyId: network.rowKeyId || randomStr(10)
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
add(type) {
|
add(type) {
|
||||||
const name = this.generateName();
|
const name = this.generateName();
|
||||||
|
|
||||||
@@ -118,7 +171,11 @@ export default {
|
|||||||
|
|
||||||
update() {
|
update() {
|
||||||
this.$emit('update:value', this.rows);
|
this.$emit('update:value', this.rows);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
unplugNIC(network) {
|
||||||
|
this.vm.unplugNIC(network.name);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -129,17 +186,28 @@ export default {
|
|||||||
v-for="(row, i) in rows"
|
v-for="(row, i) in rows"
|
||||||
:key="i"
|
:key="i"
|
||||||
>
|
>
|
||||||
<button
|
<div class="box-title mb-10">
|
||||||
v-if="!isView"
|
<h3>
|
||||||
type="button"
|
{{ t('harvester.virtualMachine.network.title') }}
|
||||||
class="role-link remove-vol"
|
</h3>
|
||||||
@click="remove(row)"
|
<button
|
||||||
>
|
v-if="!isView"
|
||||||
<i class="icon icon-x" />
|
type="button"
|
||||||
</button>
|
class="role-link btn btn-sm remove"
|
||||||
|
@click="remove(row)"
|
||||||
<h3> {{ t('harvester.virtualMachine.network.title') }} </h3>
|
>
|
||||||
|
<i class="icon icon-x" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="vm.hotplugNicFeatureEnabled && row.isHotunpluggable && isView"
|
||||||
|
type="button"
|
||||||
|
class="role-link btn btn-sm remove"
|
||||||
|
:disabled="!canCheckHotunplug"
|
||||||
|
@click="unplugNIC(row)"
|
||||||
|
>
|
||||||
|
{{ t('harvester.virtualMachine.hotUnplug.detachNIC.actionLabel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<Base
|
<Base
|
||||||
v-model:value="rows[i]"
|
v-model:value="rows[i]"
|
||||||
:rows="rows"
|
:rows="rows"
|
||||||
@@ -162,16 +230,13 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang='scss' scoped>
|
<style lang='scss' scoped>
|
||||||
.infoBox{
|
.box-title{
|
||||||
position: relative;
|
display: flex;
|
||||||
}
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.remove-vol {
|
h3 {
|
||||||
position: absolute;
|
margin-bottom: 0;
|
||||||
top: 10px;
|
}
|
||||||
right: 16px;
|
|
||||||
padding:0px;
|
|
||||||
max-height: 28px;
|
|
||||||
min-height: 28px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ 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 { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||||
|
import { OFF } from '../../../models/kubevirt.io.virtualmachine';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ['update:value'],
|
emits: ['update:value'],
|
||||||
@@ -135,6 +136,10 @@ export default {
|
|||||||
pvcs() {
|
pvcs() {
|
||||||
return this.$store.getters['harvester/all'](PVC) || [];
|
return this.$store.getters['harvester/all'](PVC) || [];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isLHV2VolExpansionFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@@ -266,7 +271,17 @@ 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.isLHV2VolExpansionFeatureEnabled) return false;
|
||||||
|
if (this.isCreate || volume.newCreateId) return false;
|
||||||
|
|
||||||
|
const isStopped = this.vm.stateDisplay === OFF;
|
||||||
|
const isLonghornV2 = this.isLonghornV2(volume);
|
||||||
|
|
||||||
|
return !isStopped || isLonghornV2;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -288,55 +303,57 @@ export default {
|
|||||||
v-for="(volume, i) in rows"
|
v-for="(volume, i) in rows"
|
||||||
:key="volume.id"
|
:key="volume.id"
|
||||||
>
|
>
|
||||||
<InfoBox class="box">
|
<InfoBox>
|
||||||
<button
|
<div class="box-title mb-10">
|
||||||
v-if="!isView"
|
<h3>
|
||||||
type="button"
|
<span
|
||||||
class="role-link btn btn-sm remove"
|
v-if="volume.to && isVirtualType"
|
||||||
@click="removeVolume(volume)"
|
class="title"
|
||||||
>
|
|
||||||
<i class="icon icon-x" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="volume.hotpluggable && isView"
|
|
||||||
type="button"
|
|
||||||
class="role-link btn remove"
|
|
||||||
@click="unplugVolume(volume)"
|
|
||||||
>
|
|
||||||
{{ t('harvester.virtualMachine.unplug.detachVolume') }}
|
|
||||||
</button>
|
|
||||||
<h3>
|
|
||||||
<span
|
|
||||||
v-if="volume.to && isVirtualType"
|
|
||||||
class="title"
|
|
||||||
>
|
|
||||||
<router-link :to="volume.to">
|
|
||||||
{{ t('harvester.virtualMachine.volume.edit') }} {{ headerFor(volume.source) }}
|
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<BadgeStateFormatter
|
|
||||||
v-if="volume.pvc"
|
|
||||||
class="ml-10 state"
|
|
||||||
:arbitrary="true"
|
|
||||||
:row="volume.pvc"
|
|
||||||
:value="volume.pvc.state"
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
v-if="dev && !!volume.pvc && !!volume.pvc.resourceExternalLink"
|
|
||||||
v-clean-tooltip="t(volume.pvc.resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
|
|
||||||
class="ml-5 resource-external"
|
|
||||||
rel="nofollow noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
:href="volume.pvc.resourceExternalLink.url"
|
|
||||||
>
|
>
|
||||||
<i class="icon icon-external-link" />
|
<router-link :to="volume.to">
|
||||||
</a>
|
{{ t('harvester.virtualMachine.volume.edit') }} {{ headerFor(volume.source) }}
|
||||||
</span>
|
</router-link>
|
||||||
|
|
||||||
<span v-else>
|
<BadgeStateFormatter
|
||||||
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
|
v-if="volume.pvc"
|
||||||
</span>
|
class="ml-10 state"
|
||||||
</h3>
|
:arbitrary="true"
|
||||||
|
:row="volume.pvc"
|
||||||
|
:value="volume.pvc.state"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
v-if="dev && !!volume.pvc && !!volume.pvc.resourceExternalLink"
|
||||||
|
v-clean-tooltip="t(volume.pvc.resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
|
||||||
|
class="ml-5 resource-external"
|
||||||
|
rel="nofollow noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
:href="volume.pvc.resourceExternalLink.url"
|
||||||
|
>
|
||||||
|
<i class="icon icon-external-link" />
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else>
|
||||||
|
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
v-if="!isView"
|
||||||
|
type="button"
|
||||||
|
class="role-link btn btn-sm remove"
|
||||||
|
@click="removeVolume(volume)"
|
||||||
|
>
|
||||||
|
<i class="icon icon-x" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="volume.hotpluggable && isView"
|
||||||
|
type="button"
|
||||||
|
class="role-link btn btn-sm remove"
|
||||||
|
@click="unplugVolume(volume)"
|
||||||
|
>
|
||||||
|
{{ t('harvester.virtualMachine.hotUnplug.detachVolume.actionLabel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<component
|
<component
|
||||||
:is="componentFor(volume.source)"
|
:is="componentFor(volume.source)"
|
||||||
@@ -347,6 +364,7 @@ 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"
|
||||||
@@ -394,7 +412,7 @@ export default {
|
|||||||
:label="ucFirst(value.volumeBackups.error.message)"
|
:label="ucFirst(value.volumeBackups.error.message)"
|
||||||
/>
|
/>
|
||||||
<Banner
|
<Banner
|
||||||
v-if="isLonghornV2(volume) && !isView"
|
v-if="!isLHV2VolExpansionFeatureEnabled && isLonghornV2(volume) && !isView"
|
||||||
color="warning"
|
color="warning"
|
||||||
:label="t('harvester.volume.longhorn.disableResize')"
|
:label="t('harvester.volume.longhorn.disableResize')"
|
||||||
/>
|
/>
|
||||||
@@ -479,25 +497,24 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang='scss' scoped>
|
<style lang='scss' scoped>
|
||||||
.box {
|
.box-title {
|
||||||
position: relative;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.state {
|
.state {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.remove {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bootOrder {
|
.bootOrder {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
+6
-8
@@ -99,7 +99,6 @@ export default {
|
|||||||
this.allPVCs
|
this.allPVCs
|
||||||
.filter( (pvc) => {
|
.filter( (pvc) => {
|
||||||
let isAvailable = true;
|
let isAvailable = true;
|
||||||
let isBeingUsed = false;
|
|
||||||
|
|
||||||
this.rows.forEach( (O) => {
|
this.rows.forEach( (O) => {
|
||||||
if (O.volumeName === pvc.metadata.name) {
|
if (O.volumeName === pvc.metadata.name) {
|
||||||
@@ -111,17 +110,16 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// already used as image volume
|
||||||
|
if (this.idx > 0 && pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (pvc.isGoldenImageVolume) {
|
if (pvc.isGoldenImageVolume) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pvc.attachVM && isAvailable && pvc.attachVM?.id === this.vm?.id && this.isEdit) {
|
return isAvailable && pvc.isAvailable;
|
||||||
isBeingUsed = false;
|
|
||||||
} else if (pvc.attachVM) {
|
|
||||||
isBeingUsed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isAvailable && !isBeingUsed && pvc.isAvailable;
|
|
||||||
})
|
})
|
||||||
.map((pvc) => {
|
.map((pvc) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -64,7 +64,12 @@ export default {
|
|||||||
isVirtualType: {
|
isVirtualType: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
}
|
},
|
||||||
|
|
||||||
|
isResizeDisabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -135,10 +140,6 @@ export default {
|
|||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
return this.$store.getters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
||||||
},
|
},
|
||||||
|
|
||||||
isLonghornV2() {
|
|
||||||
return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.isLonghornV2;
|
|
||||||
},
|
|
||||||
|
|
||||||
selectedImage() {
|
selectedImage() {
|
||||||
return this.$store.getters['harvester/all'](HCI.IMAGE)?.find( (I) => this.value.image === I.id);
|
return this.$store.getters['harvester/all'](HCI.IMAGE)?.find( (I) => this.value.image === I.id);
|
||||||
},
|
},
|
||||||
@@ -350,7 +351,7 @@ export default {
|
|||||||
:label="t('harvester.fields.size')"
|
:label="t('harvester.fields.size')"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:required="validateRequired"
|
:required="validateRequired"
|
||||||
:disable="isLonghornV2"
|
:disabled="isResizeDisabled"
|
||||||
:suffix="GIBIBYTE"
|
:suffix="GIBIBYTE"
|
||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ 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 { GIBIBYTE } from '../../../../utils/unit';
|
||||||
|
import { isInternalStorageClass } from '../../../../utils/storage-class';
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -55,7 +56,12 @@ export default {
|
|||||||
isVirtualType: {
|
isVirtualType: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
}
|
},
|
||||||
|
|
||||||
|
isResizeDisabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -118,19 +124,22 @@ export default {
|
|||||||
|
|
||||||
storageClassOptions() {
|
storageClassOptions() {
|
||||||
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
let 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
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
},
|
},
|
||||||
|
|
||||||
isLonghornV2() {
|
|
||||||
return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.isLonghornV2;
|
|
||||||
},
|
|
||||||
|
|
||||||
isLonghornStorageClass() {
|
isLonghornStorageClass() {
|
||||||
const selectedSC = this.storageClasses.find((sc) => sc.name === this.value?.storageClassName) || {};
|
const selectedSC = this.storageClasses.find((sc) => sc.name === this.value?.storageClassName) || {};
|
||||||
|
|
||||||
@@ -302,7 +311,7 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
:required="validateRequired"
|
:required="validateRequired"
|
||||||
:label="t('harvester.fields.size')"
|
:label="t('harvester.fields.size')"
|
||||||
:disabled="isLonghornV2"
|
:disabled="isResizeDisabled"
|
||||||
:suffix="GIBIBYTE"
|
:suffix="GIBIBYTE"
|
||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
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 { clone } 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';
|
||||||
@@ -96,7 +96,6 @@ export default {
|
|||||||
templateVersionId: '',
|
templateVersionId: '',
|
||||||
namePrefix: '',
|
namePrefix: '',
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
useTemplate: false,
|
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
hostname,
|
hostname,
|
||||||
isRestartImmediately,
|
isRestartImmediately,
|
||||||
@@ -255,6 +254,7 @@ export default {
|
|||||||
return volume;
|
return volume;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
delete cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.MAC_ADDRESS];
|
||||||
cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
||||||
|
|
||||||
// Update instance labels, labels and annotations
|
// Update instance labels, labels and annotations
|
||||||
@@ -338,6 +338,15 @@ export default {
|
|||||||
saveVM(buttonCb) {
|
saveVM(buttonCb) {
|
||||||
clear(this.errors);
|
clear(this.errors);
|
||||||
|
|
||||||
|
this.validateCPUMemory();
|
||||||
|
|
||||||
|
// block create VM flow if has validation errors
|
||||||
|
if (this.errors.length) {
|
||||||
|
buttonCb(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isSingle) {
|
if (this.isSingle) {
|
||||||
this.saveSingle(buttonCb);
|
this.saveSingle(buttonCb);
|
||||||
} else {
|
} else {
|
||||||
@@ -345,6 +354,18 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
validateCPUMemory() {
|
||||||
|
const { cpu, memory } = this;
|
||||||
|
|
||||||
|
if ((!cpu)) {
|
||||||
|
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.cpu') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((!memory)) {
|
||||||
|
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.memory') }, true));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async saveSingle(buttonCb) {
|
async saveSingle(buttonCb) {
|
||||||
this.parseVM();
|
this.parseVM();
|
||||||
this.value.spec.template.spec.hostname = this.hostname ? this.hostname : this.value.metadata.name;
|
this.value.spec.template.spec.hostname = this.hostname ? this.hostname : this.value.metadata.name;
|
||||||
@@ -413,38 +434,44 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_compareDisksIgnoreStorage(disksA = [], disksB = []) {
|
||||||
|
if (disksA.length !== disksB.length) return false;
|
||||||
|
|
||||||
|
const sortedA = [...disksA].sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
|
||||||
|
const sortedB = [...disksB].sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
|
||||||
|
|
||||||
|
for (let i = 0; i < sortedA.length; i++) {
|
||||||
|
const cloneA = clone(sortedA[i]);
|
||||||
|
const cloneB = clone(sortedB[i]);
|
||||||
|
|
||||||
|
// remove the storage field before comparison
|
||||||
|
delete cloneA?.spec?.resources?.requests?.storage;
|
||||||
|
delete cloneB?.spec?.resources?.requests?.storage;
|
||||||
|
|
||||||
|
if (!isEqual(cloneA, cloneB)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
_shouldPromptRestart(oldVM, newVM) {
|
||||||
|
const specChanged = !isEqual(oldVM?.spec, newVM?.spec);
|
||||||
|
|
||||||
|
// check if the disk defined in annotations has changed, ignoring storage size
|
||||||
|
const oldDisks = parseVolumeClaimTemplates(oldVM);
|
||||||
|
const newDisks = parseVolumeClaimTemplates(newVM);
|
||||||
|
const diskChanged = !this._compareDisksIgnoreStorage(oldDisks, newDisks);
|
||||||
|
|
||||||
|
return specChanged || diskChanged;
|
||||||
|
},
|
||||||
|
|
||||||
restartVM() {
|
restartVM() {
|
||||||
if (this.mode !== 'edit') {
|
if (this.mode !== 'edit' || !this.value.isRunning) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.value.isRunning) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const cloneDeepNewVM = clone(this.value);
|
|
||||||
|
|
||||||
// new VM
|
const needsRestart = this._shouldPromptRestart(this.cloneVM, this.value);
|
||||||
delete cloneDeepNewVM?.metadata;
|
|
||||||
delete cloneDeepNewVM?.__clone;
|
|
||||||
|
|
||||||
// old VM
|
// if no restart is needed (either no changes or only size changed)
|
||||||
delete this.cloneVM?.metadata;
|
if (!needsRestart) return;
|
||||||
delete this.cloneVM?.__clone;
|
|
||||||
|
|
||||||
// add empty hostDevices to old VM as CRD does not have it.
|
|
||||||
const devicesObj = this.cloneVM?.spec?.template?.spec?.domain?.devices;
|
|
||||||
|
|
||||||
if (devicesObj && devicesObj.hostDevices === undefined) {
|
|
||||||
set(devicesObj, 'hostDevices', []);
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldVM = JSON.parse(JSON.stringify(this.cloneVM));
|
|
||||||
const newVM = JSON.parse(JSON.stringify(cloneDeepNewVM));
|
|
||||||
|
|
||||||
// we won't show restart dialog in yaml page as we don't have a way to detect change in yaml editor.
|
|
||||||
// only check VM is changed in form page.
|
|
||||||
if (isEqual(oldVM, newVM)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.isOpen = true;
|
this.isOpen = true;
|
||||||
@@ -463,6 +490,7 @@ export default {
|
|||||||
if (this.isSingle) {
|
if (this.isSingle) {
|
||||||
if (!this.value.spec.template.spec.hostname) {
|
if (!this.value.spec.template.spec.hostname) {
|
||||||
this.value.spec.template.spec['hostname'] = this.value.metadata.name;
|
this.value.spec.template.spec['hostname'] = this.value.metadata.name;
|
||||||
|
this.spec.template.spec['hostname'] = this.value.metadata.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,9 +631,13 @@ 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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -867,7 +899,10 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col span-6">
|
<div
|
||||||
|
v-if="!value.vmMachineTypeAutoFeatureEnabled"
|
||||||
|
class="col span-6"
|
||||||
|
>
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="machineType"
|
v-model:value="machineType"
|
||||||
label-key="harvester.virtualMachine.input.MachineType"
|
label-key="harvester.virtualMachine.input.MachineType"
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ export default {
|
|||||||
return this.mode === _VIEW;
|
return this.mode === _VIEW;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
disableAdd() {
|
||||||
|
return this.isStandaloneHarvester && this.rows.some((row) => row.namespace === '*');
|
||||||
|
},
|
||||||
|
|
||||||
showAdd() {
|
showAdd() {
|
||||||
return !this.isView;
|
return !this.isView;
|
||||||
},
|
},
|
||||||
@@ -141,6 +145,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
:disabled="disableAdd"
|
||||||
class="btn role-tertiary add"
|
class="btn role-tertiary add"
|
||||||
@click="add()"
|
@click="add()"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -66,17 +66,26 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
namespaceOptions() {
|
namespaceOptions() {
|
||||||
const out = (this.filteredNamespaces || []).map((namespace) => {
|
const namespaces = this.filteredNamespaces;
|
||||||
return {
|
const selected = this.rows.map((r) => r?.namespace);
|
||||||
label: namespace.metadata.name,
|
|
||||||
value: namespace.id,
|
if (this.isStandaloneHarvester) {
|
||||||
};
|
return [
|
||||||
});
|
{ label: this.t('generic.all'), value: '*' },
|
||||||
|
...namespaces.map((ns) => ({
|
||||||
|
label: ns.metadata.name,
|
||||||
|
value: ns.id,
|
||||||
|
disabled: selected.includes(ns.id) && ns.id !== this.row.namespace
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return [{
|
return [{
|
||||||
label: this.t('generic.all'),
|
label: this.t('generic.all'),
|
||||||
value: '*',
|
value: '*',
|
||||||
}, ...out];
|
},
|
||||||
|
...namespaces.map((ns) => ({ label: ns.metadata.name, value: ns.id }))
|
||||||
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
guestClusterOptions() {
|
guestClusterOptions() {
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<script>
|
||||||
|
import NetworkPolicy from '@shell/edit/networking.k8s.io.networkpolicy';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { DOC } from '../config/doc-links';
|
||||||
|
import { docLink } from '../utils/feature-flags';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
NetworkPolicy,
|
||||||
|
Banner
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
networkPolicyDocLink() {
|
||||||
|
const version = this.$store.getters['harvester-common/getServerVersion']();
|
||||||
|
|
||||||
|
return docLink(DOC.NETWORK_POLICY, version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="network-policy-edit">
|
||||||
|
<Banner color="info">
|
||||||
|
<t
|
||||||
|
k="harvester.networkPolicy.banner"
|
||||||
|
:url="networkPolicyDocLink"
|
||||||
|
:raw="true"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
<NetworkPolicy v-bind="$attrs" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.network-policy-edit {
|
||||||
|
flex: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+178
-16
@@ -154,11 +154,19 @@ harvester:
|
|||||||
nodeTimeout:
|
nodeTimeout:
|
||||||
label: Node Collection Timeout
|
label: Node Collection Timeout
|
||||||
tooltip: Minutes allowed for collecting logs/configurations on nodes.<br/>See docs support-bundle-node-collection-timeout for detail.
|
tooltip: Minutes allowed for collecting logs/configurations on nodes.<br/>See docs support-bundle-node-collection-timeout for detail.
|
||||||
hotplug:
|
hotplugVolume:
|
||||||
success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
|
success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
|
||||||
title: Add Volume
|
title: Add Volume
|
||||||
hotunplug:
|
hotplugNic:
|
||||||
success: 'Volume { name } is detached successfully.'
|
success: 'The settings have been saved, but the network interface {interfaceName} will be attached only after the virtual machine is migrated.'
|
||||||
|
title: Add Network Interface
|
||||||
|
vmNetwork: Virtual Machine Network
|
||||||
|
macAddress: MAC Address
|
||||||
|
macAddressTooltip: If left blank, the MAC address will be automatically generated.
|
||||||
|
cpuMemoryHotplug:
|
||||||
|
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 }.'
|
||||||
snapshot:
|
snapshot:
|
||||||
title: Take Snapshot
|
title: Take Snapshot
|
||||||
name: Name
|
name: Name
|
||||||
@@ -213,6 +221,7 @@ 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
|
||||||
@@ -221,7 +230,8 @@ harvester:
|
|||||||
disableCPUManager: Disable CPU Manager
|
disableCPUManager: Disable CPU Manager
|
||||||
cordon: Cordon
|
cordon: Cordon
|
||||||
uncordon: Uncordon
|
uncordon: Uncordon
|
||||||
addHotplug: Add Volume
|
addHotplugVolume: Add Volume
|
||||||
|
addHotplugNic: Hotplug Network Interface
|
||||||
exportImage: Export Image
|
exportImage: Export Image
|
||||||
viewlogs: View Logs
|
viewlogs: View Logs
|
||||||
cancelExpand: Cancel Expand
|
cancelExpand: Cancel Expand
|
||||||
@@ -231,6 +241,7 @@ harvester:
|
|||||||
shutdown: Shut Down
|
shutdown: Shut Down
|
||||||
powerOn: Power On
|
powerOn: Power On
|
||||||
reboot: Reboot
|
reboot: Reboot
|
||||||
|
reconnect: Reconnect
|
||||||
forceStop: Force Stop
|
forceStop: Force Stop
|
||||||
formatters:
|
formatters:
|
||||||
hardwareResourceGauge:
|
hardwareResourceGauge:
|
||||||
@@ -266,6 +277,7 @@ harvester:
|
|||||||
cronExpression: Cron Expression
|
cronExpression: Cron Expression
|
||||||
retain: Retain
|
retain: Retain
|
||||||
scheduleType: Type
|
scheduleType: Type
|
||||||
|
configuredProfiles: Configured Profiles
|
||||||
maxFailure: Max Failure
|
maxFailure: Max Failure
|
||||||
sourceVm: Source Virtual Machine
|
sourceVm: Source Virtual Machine
|
||||||
vmSchedule: Virtual Machine Schedule
|
vmSchedule: Virtual Machine Schedule
|
||||||
@@ -398,7 +410,7 @@ harvester:
|
|||||||
sha512: 'Invalid SHA512 checksum.'
|
sha512: 'Invalid SHA512 checksum.'
|
||||||
subnet:
|
subnet:
|
||||||
excludeIps: 'Exclude IPs cannot be empty. Please remove or fill in the exclude IPs.'
|
excludeIps: 'Exclude IPs cannot be empty. Please remove or fill in the exclude IPs.'
|
||||||
|
aclEmptyError: The fields in subnet access control list rule can not be empty.
|
||||||
dashboard:
|
dashboard:
|
||||||
label: Dashboard
|
label: Dashboard
|
||||||
header: "Harvester Cluster: {cluster}"
|
header: "Harvester Cluster: {cluster}"
|
||||||
@@ -435,6 +447,7 @@ harvester:
|
|||||||
storage: Storage
|
storage: Storage
|
||||||
labels: Labels
|
labels: Labels
|
||||||
ksmtuned: Ksmtuned
|
ksmtuned: Ksmtuned
|
||||||
|
hugepages: Hugepages
|
||||||
seeder: Out-of-band Access
|
seeder: Out-of-band Access
|
||||||
detail:
|
detail:
|
||||||
kvm:
|
kvm:
|
||||||
@@ -507,6 +520,20 @@ harvester:
|
|||||||
fullScans: Full Scans
|
fullScans: Full Scans
|
||||||
stableNodeChains: Stable Node Chains
|
stableNodeChains: Stable Node Chains
|
||||||
stableNodeDups: Stable Node Dups
|
stableNodeDups: Stable Node Dups
|
||||||
|
hugepages:
|
||||||
|
meminfo: Meminfo
|
||||||
|
transparent:
|
||||||
|
title: Transparent Hugepages <a href="{url}" target="_blank"><i class="icon icon-info" /></a>
|
||||||
|
enabled: Enabled
|
||||||
|
shmemEnabled: Shared Memory Enabled
|
||||||
|
defrag: Defragmentation
|
||||||
|
status:
|
||||||
|
anon: Anonymous Hugepages (bytes)
|
||||||
|
size: Default Hugepage Size (bytes)
|
||||||
|
total: Total Hugepages
|
||||||
|
free: Free Hugepages
|
||||||
|
rsvd: Reserved Hugepages
|
||||||
|
surp: Surplus Hugepages
|
||||||
disk:
|
disk:
|
||||||
add: Add Disk
|
add: Add Disk
|
||||||
path:
|
path:
|
||||||
@@ -571,6 +598,20 @@ 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
|
||||||
|
hotUnplug:
|
||||||
|
actionLabel: Detach
|
||||||
|
detachVolume:
|
||||||
|
title: 'Are you sure that you want to detach volume {name}?'
|
||||||
|
actionLabel: Detach Volume
|
||||||
|
success: 'Volume { name } is detached successfully.'
|
||||||
|
detachNIC:
|
||||||
|
title: 'Are you sure that you want to detach network interface {name}?'
|
||||||
|
actionLabel: Detach Network Interface
|
||||||
|
success: 'The settings have been saved, but the network interface {name} will be detached only after the virtual machine is migrated.'
|
||||||
instance:
|
instance:
|
||||||
singleInstance:
|
singleInstance:
|
||||||
multipleInstance:
|
multipleInstance:
|
||||||
@@ -602,11 +643,6 @@ harvester:
|
|||||||
title: 'Select the volume you want to delete:'
|
title: 'Select the volume you want to delete:'
|
||||||
deleteAll: Delete All
|
deleteAll: Delete All
|
||||||
tips: "Warn: The snapshots of the virtual machine will be deleted with virtual machine and the snapshots of volume will be deleted with volume."
|
tips: "Warn: The snapshots of the virtual machine will be deleted with virtual machine and the snapshots of volume will be deleted with volume."
|
||||||
unplug:
|
|
||||||
title: 'Are you sure that you want to detach volume {name} ?'
|
|
||||||
actionLabel: Detach
|
|
||||||
detachVolume:
|
|
||||||
Detach Volume
|
|
||||||
restartTip: |-
|
restartTip: |-
|
||||||
{restart, select,
|
{restart, select,
|
||||||
true {Restart}
|
true {Restart}
|
||||||
@@ -716,6 +752,9 @@ 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
|
||||||
@@ -824,6 +863,7 @@ harvester:
|
|||||||
snapshots: Snapshots
|
snapshots: Snapshots
|
||||||
datasource: Data Source
|
datasource: Data Source
|
||||||
details: Details
|
details: Details
|
||||||
|
conditions: Conditions
|
||||||
size: Size
|
size: Size
|
||||||
volumeMode: Volume Mode
|
volumeMode: Volume Mode
|
||||||
source: Source
|
source: Source
|
||||||
@@ -882,6 +922,8 @@ harvester:
|
|||||||
checksumTip: Validate the image using the SHA512 checksum, if specified.
|
checksumTip: Validate the image using the SHA512 checksum, if specified.
|
||||||
tooltip:
|
tooltip:
|
||||||
imported: Created automatically by the vm-import-controller
|
imported: Created automatically by the vm-import-controller
|
||||||
|
errors:
|
||||||
|
unsupportedBackend: 'Unsupported backend type: {backend}'
|
||||||
|
|
||||||
vmTemplate:
|
vmTemplate:
|
||||||
label: Templates
|
label: Templates
|
||||||
@@ -1017,7 +1059,7 @@ harvester:
|
|||||||
progress: Restore in progress
|
progress: Restore in progress
|
||||||
complete: Restore completed
|
complete: Restore completed
|
||||||
|
|
||||||
subnet:
|
subnet:
|
||||||
cidrBlock:
|
cidrBlock:
|
||||||
tooltip: The subnet range in CIDR notation. Note that the CIDR blocks of different Subnets' within the same VPC cannot overlap.
|
tooltip: The subnet range in CIDR notation. Note that the CIDR blocks of different Subnets' within the same VPC cannot overlap.
|
||||||
label: CIDR Block
|
label: CIDR Block
|
||||||
@@ -1032,6 +1074,13 @@ harvester:
|
|||||||
gateway:
|
gateway:
|
||||||
label: Gateway IP
|
label: Gateway IP
|
||||||
placeholder: e.g. 172.20.0.1
|
placeholder: e.g. 172.20.0.1
|
||||||
|
dhcp:
|
||||||
|
label: Dynamic Host Configuration Protocol (DHCP)
|
||||||
|
v4Options: DHCPV4Options
|
||||||
|
v6Options: DHCPV6Options
|
||||||
|
placeholder: key1=value1, key2=value2
|
||||||
|
dhcpOptionBanner: DHCP options is a key/value string concatenate with comma. For more detail, please refer to <a href="https://kubeovn.github.io/docs/v1.13.x/en/kubevirt/dhcp/" target="_blank">KubOVN document</a>
|
||||||
|
tooltip: Enable DHCP server for this subnet. When enabled, VMs can automatically obtain IP addresses from this subnet.
|
||||||
private:
|
private:
|
||||||
label: Private Subnet
|
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.
|
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.
|
||||||
@@ -1042,6 +1091,25 @@ harvester:
|
|||||||
placeholder: e.g. 172.16.0.0/16
|
placeholder: e.g. 172.16.0.0/16
|
||||||
excludeIPs:
|
excludeIPs:
|
||||||
tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list.
|
tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list.
|
||||||
|
placeholder: single IP or 192.168.0.100..192.168.0.200
|
||||||
|
|
||||||
|
acl:
|
||||||
|
label: Access Control List
|
||||||
|
tooltip: The ACL to apply to this Subnet. Must be one of the ACLs in the same namespace.
|
||||||
|
action:
|
||||||
|
label: Action
|
||||||
|
placeholder: Please select an action
|
||||||
|
direction:
|
||||||
|
label: Direction
|
||||||
|
placeholder: Please select a direction
|
||||||
|
addRule: Add Rule
|
||||||
|
priority:
|
||||||
|
label: Priority
|
||||||
|
placeholder: Please select a priority
|
||||||
|
match:
|
||||||
|
label: Match
|
||||||
|
placeholder: e.g. ip4.dst == 10.10.0.2
|
||||||
|
banner: The supported field in ACL match can refer to <a href="https://kubeovn.github.io/docs/v1.14.x/en/guide/subnet/#subnet-acl" target="_blank">KubeOvn Subnet ACL document</a>
|
||||||
|
|
||||||
vpc:
|
vpc:
|
||||||
noAddonEnabled:
|
noAddonEnabled:
|
||||||
@@ -1068,8 +1136,10 @@ harvester:
|
|||||||
placeholder: e.g. 169.254.0.1/16
|
placeholder: e.g. 169.254.0.1/16
|
||||||
remoteVpc:
|
remoteVpc:
|
||||||
label: Remote VPC
|
label: Remote VPC
|
||||||
|
infoBanner: The static route destination CIDR must cover all subnets CIDR from remote VPC Peer. Read <a href="{url}" target="_blank">VPC Peering Configuration Examples</a> for more information.
|
||||||
|
networkPolicy:
|
||||||
|
label: Network Policies
|
||||||
|
banner: The network policies must be used for VMs attached to overlay networks. Please read the <a href="{url}" target="_blank">harvester document</a> how the network policy works.
|
||||||
network:
|
network:
|
||||||
label: Virtual Machine Networks
|
label: Virtual Machine Networks
|
||||||
tabs:
|
tabs:
|
||||||
@@ -1126,6 +1196,9 @@ 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
|
||||||
@@ -1135,7 +1208,7 @@ harvester:
|
|||||||
vlan: VLAN ID
|
vlan: VLAN ID
|
||||||
exclude:
|
exclude:
|
||||||
label: Exclude IPs
|
label: Exclude IPs
|
||||||
placeholder: e.g. 172.16.0.1
|
placeholder: CIDR format, e.g. 172.16.0.10/32
|
||||||
invalid: '"Exclude list" is invalid.'
|
invalid: '"Exclude list" is invalid.'
|
||||||
addIp: Add Exclude IP
|
addIp: Add Exclude IP
|
||||||
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
||||||
@@ -1221,10 +1294,31 @@ harvester:
|
|||||||
retention: How long to retain metrics
|
retention: How long to retain metrics
|
||||||
retentionSize: Maximum size of metrics
|
retentionSize: Maximum size of metrics
|
||||||
clusterRegistrationUrl:
|
clusterRegistrationUrl:
|
||||||
|
url: URL
|
||||||
|
insecureSkipTLSVerify: Insecure Skip TLS Verify
|
||||||
|
tip:
|
||||||
|
prefix: Harvester secures cluster registration via TLS by default. If opt out "Insecure SKip TLS Verify", you must provide custom CA certificates using the
|
||||||
|
middle: 'additional-ca'
|
||||||
|
suffix: setting.
|
||||||
message: To completely unset the imported Harvester cluster, please also remove it on the Rancher Dashboard UI via the <code> Virtualization Management </code> page.
|
message: To completely unset the imported Harvester cluster, please also remove it on the Rancher Dashboard UI via the <code> Virtualization Management </code> page.
|
||||||
ntpServers:
|
ntpServers:
|
||||||
isNotIPV4: The address you entered is not IPv4 or host. Please enter a valid IPv4 address or a host address.
|
isNotIPV4: The address you entered is not IPv4 or host. Please enter a valid IPv4 address or a host address.
|
||||||
isDuplicate: There are duplicate NTP server configurations.
|
isDuplicate: There are duplicate NTP server configurations.
|
||||||
|
kubevirtMigration:
|
||||||
|
parseError: "Failed to parse configuration: {error}"
|
||||||
|
parallelMigrationsPerCluster: "Parallel Migrations Per Cluster"
|
||||||
|
parallelOutboundMigrationsPerNode: "Parallel Outbound Migrations Per Node"
|
||||||
|
bandwidthPerMigration: "Bandwidth Per Migration"
|
||||||
|
bandwidthPerMigrationTooltip: '0 is KubeVirt default, representing unlimited bandwidth'
|
||||||
|
completionTimeoutPerGiB: "Completion Timeout Per GiB"
|
||||||
|
progressTimeout: "Progress Timeout"
|
||||||
|
allowAutoConverge: "Allow Auto Converge"
|
||||||
|
allowPostCopy: "Allow Post Copy"
|
||||||
|
unsafeMigrationOverride: "Unsafe Migration Override"
|
||||||
|
allowWorkloadDisruption: "Allow Workload Disruption"
|
||||||
|
disableTLS: "Disable TLS"
|
||||||
|
matchSELinuxLevelOnMigration: "Match SELinux Level On Migration"
|
||||||
|
|
||||||
cloudTemplate:
|
cloudTemplate:
|
||||||
label: Cloud Configuration Templates
|
label: Cloud Configuration Templates
|
||||||
templateType: Template Type
|
templateType: Template Type
|
||||||
@@ -1369,6 +1463,32 @@ 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
|
||||||
@@ -1414,6 +1534,14 @@ harvester:
|
|||||||
vlanStatus:
|
vlanStatus:
|
||||||
vlanConfig:
|
vlanConfig:
|
||||||
label: Network Configuration
|
label: Network Configuration
|
||||||
|
l2VlanMode:
|
||||||
|
access: Access
|
||||||
|
trunk: Trunk
|
||||||
|
label: Mode
|
||||||
|
vlanTrunk:
|
||||||
|
minId: Minimum VLAN ID
|
||||||
|
maxId: Maximum VLAN ID
|
||||||
|
add: Add VLAN Trunk
|
||||||
|
|
||||||
clusterNetwork:
|
clusterNetwork:
|
||||||
title: Cluster Network Configuration
|
title: Cluster Network Configuration
|
||||||
@@ -1441,6 +1569,7 @@ harvester:
|
|||||||
'harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
|
'harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
|
||||||
'harvester-system/harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
|
'harvester-system/harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
|
||||||
'harvester-csi-driver-lvm': harvester-csi-driver-lvm is an add-on allowing users to create PVC through the LVM with local devices.
|
'harvester-csi-driver-lvm': harvester-csi-driver-lvm is an add-on allowing users to create PVC through the LVM with local devices.
|
||||||
|
'descheduler': 'The virtual machine auto balance optimizes workload scheduling by evicting pods that are not optimally placed according to administrator-defined policies.'
|
||||||
vmImport:
|
vmImport:
|
||||||
titles:
|
titles:
|
||||||
basic: Basic
|
basic: Basic
|
||||||
@@ -1587,6 +1716,26 @@ harvester:
|
|||||||
middle: here
|
middle: here
|
||||||
suffix: to enable it to manage your SR-IOV GPU devices.
|
suffix: to enable it to manage your SR-IOV GPU devices.
|
||||||
|
|
||||||
|
migconfiguration:
|
||||||
|
label: vGPU MIG Configurations
|
||||||
|
infoBanner: To configure the MIG configuration, please disable it first and re-enable after editing the configuration.
|
||||||
|
profileSpec: Profile Specs
|
||||||
|
profileStatus: Profile Status
|
||||||
|
tableHeaders:
|
||||||
|
profileName: Profile Name
|
||||||
|
total: Total
|
||||||
|
vGPUID: vGPU ID
|
||||||
|
available: Available
|
||||||
|
requested: Requested
|
||||||
|
requested: Requested
|
||||||
|
available: Available
|
||||||
|
total: Total
|
||||||
|
vGPUID: vGPU ID
|
||||||
|
goSriovGPU:
|
||||||
|
prefix: Please enable the supported GPU devices in
|
||||||
|
middle: SR-IOV GPU Devices
|
||||||
|
suffix: page to manage the vGPU MIG configurations.
|
||||||
|
|
||||||
vgpu:
|
vgpu:
|
||||||
label: vGPU Devices
|
label: vGPU Devices
|
||||||
noPermission: Please contact system administrator to add Harvester add-ons first.
|
noPermission: Please contact system administrator to add Harvester add-ons first.
|
||||||
@@ -1679,9 +1828,8 @@ advancedSettings:
|
|||||||
'harv-server-version': Harvester server version.
|
'harv-server-version': Harvester server version.
|
||||||
'harv-upgrade-checker-enabled': Specifies whether to enable Harvester upgrade check or not. Default is True.
|
'harv-upgrade-checker-enabled': Specifies whether to enable Harvester upgrade check or not. Default is True.
|
||||||
'harv-upgrade-checker-url': Default Harvester upgrade check url. Only used when the <code>upgrade-checker-enabled</code> is equal to True.
|
'harv-upgrade-checker-url': Default Harvester upgrade check url. Only used when the <code>upgrade-checker-enabled</code> is equal to True.
|
||||||
'harv-ui-source': Configure how to load the UI source.
|
'harv-ui-source': Configure how to load the UI source. The `External` option is used to load the UI from a ui-index URL, while the `Bundled` option loads the UI from the local Harvester server.
|
||||||
'harv-ui-index': 'HTML index location for the UI.'
|
'harv-ui-index': 'HTML index location for the UI.'
|
||||||
'harv-ui-plugin-index': 'JS index location for the Harvester plugin UI.'
|
|
||||||
'harv-cluster-registration-url': Registration URL for multi-cluster management.
|
'harv-cluster-registration-url': Registration URL for multi-cluster management.
|
||||||
'harv-http-proxy': 'HTTP proxy for Harvester to access external services.'
|
'harv-http-proxy': 'HTTP proxy for Harvester to access external services.'
|
||||||
'harv-additional-ca': 'Custom CA root certificates for TLS validation.'
|
'harv-additional-ca': 'Custom CA root certificates for TLS validation.'
|
||||||
@@ -1704,6 +1852,9 @@ advancedSettings:
|
|||||||
'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-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-vm-migration-network': 'Segregated network for VM migration traffic.'
|
||||||
|
'harv-rancher-cluster': 'Configure Rancher cluster integration settings for guest cluster management.'
|
||||||
|
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
|
||||||
|
'harv-kubevirt-migration': 'Configure cluster-wide KubeVirt live migration parameters.'
|
||||||
|
|
||||||
typeLabel:
|
typeLabel:
|
||||||
kubevirt.io.virtualmachine: |-
|
kubevirt.io.virtualmachine: |-
|
||||||
@@ -1766,6 +1917,11 @@ typeLabel:
|
|||||||
one { Virtual Private Cloud }
|
one { Virtual Private Cloud }
|
||||||
other { Virtual Private Clouds }
|
other { Virtual Private Clouds }
|
||||||
}
|
}
|
||||||
|
networking.k8s.io.networkpolicy: |-
|
||||||
|
{count, plural,
|
||||||
|
one { Network Policy }
|
||||||
|
other { Network Policies }
|
||||||
|
}
|
||||||
harvesterhci.io.cloudtemplate: |-
|
harvesterhci.io.cloudtemplate: |-
|
||||||
{count, plural,
|
{count, plural,
|
||||||
one { Cloud Configuration Template }
|
one { Cloud Configuration Template }
|
||||||
@@ -1854,6 +2010,12 @@ typeLabel:
|
|||||||
other { vGPU Devices }
|
other { vGPU Devices }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
devices.harvesterhci.io.migconfiguration: |-
|
||||||
|
{count, plural,
|
||||||
|
one { vGPU MIG Configuration }
|
||||||
|
other { vGPU MIG Configurations }
|
||||||
|
}
|
||||||
|
|
||||||
harvesterhci.io.secret: |-
|
harvesterhci.io.secret: |-
|
||||||
{count, plural,
|
{count, plural,
|
||||||
one { Secret }
|
one { Secret }
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<script>
|
||||||
|
import { STATE, NAME } from '@shell/config/table-headers';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import Banner from '@components/Banner/Banner.vue';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import ResourceTable from '@shell/components/ResourceTable';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import { ADD_ONS } from '../config/harvester-map';
|
||||||
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ListMIGConfigurations',
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Banner,
|
||||||
|
Loading,
|
||||||
|
ResourceTable,
|
||||||
|
MessageLink,
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
this.schema = this.$store.getters[`${ inStore }/schemaFor`](HCI.MIG_CONFIGURATION);
|
||||||
|
this.hasAddonSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.ADD_ONS);
|
||||||
|
|
||||||
|
if (this.hasSchema) {
|
||||||
|
try {
|
||||||
|
const hash = await allHash({
|
||||||
|
migconfigs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.MIG_CONFIGURATION }),
|
||||||
|
vGpuDevices: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VGPU_DEVICE }),
|
||||||
|
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS })
|
||||||
|
});
|
||||||
|
|
||||||
|
this.hasPCIAddon = hash.addons.find((addon) => addon.name === ADD_ONS.PCI_DEVICE_CONTROLLER)?.spec?.enabled === true;
|
||||||
|
this.hasSriovgpuAddon = hash.addons.find((addon) => addon.name === ADD_ONS.NVIDIA_DRIVER_TOOLKIT_CONTROLLER)?.spec?.enabled === true;
|
||||||
|
this.hasSRIOVGPUSchema = !!this.$store.getters[`${ inStore }/schemaFor`](HCI.SR_IOVGPU_DEVICE);
|
||||||
|
|
||||||
|
if (this.hasSRIOVGPUSchema) {
|
||||||
|
await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOVGPU_DEVICE });
|
||||||
|
}
|
||||||
|
this.rows = hash.migconfigs;
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rows: [],
|
||||||
|
schema: null,
|
||||||
|
hasAddonSchema: false,
|
||||||
|
hasPCIAddon: false,
|
||||||
|
hasSriovgpuAddon: false,
|
||||||
|
hasSRIOVGPUSchema: false,
|
||||||
|
toVGpuAddon: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.NVIDIA_DRIVER_TOOLKIT_CONTROLLER }?mode=edit`,
|
||||||
|
toPciAddon: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.PCI_DEVICE_CONTROLLER }?mode=edit`,
|
||||||
|
SRIOVGPUPage: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.NVIDIA_DRIVER_TOOLKIT_CONTROLLER }?mode=edit`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
hasSchema() {
|
||||||
|
return !!this.schema;
|
||||||
|
},
|
||||||
|
|
||||||
|
rowsData() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const rows = this.$store.getters[`${ inStore }/all`](HCI.MIG_CONFIGURATION) || [];
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
},
|
||||||
|
|
||||||
|
sriovGPUPage() {
|
||||||
|
return {
|
||||||
|
name: 'harvester-c-cluster-resource',
|
||||||
|
params: { cluster: this.$store.getters['clusterId'], resource: HCI.SR_IOVGPU_DEVICE },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
showEnableSRIOVGPUMessage() {
|
||||||
|
return this.rowsData.length === 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
headers() {
|
||||||
|
const cols = [
|
||||||
|
STATE,
|
||||||
|
NAME,
|
||||||
|
{
|
||||||
|
name: 'address',
|
||||||
|
label: 'Address',
|
||||||
|
value: 'spec.gpuAddress',
|
||||||
|
sort: ['spec.gpuAddress']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Configured Profile',
|
||||||
|
label: 'Configured Count',
|
||||||
|
labelKey: 'harvester.tableHeaders.configuredProfiles',
|
||||||
|
value: 'configuredProfiles',
|
||||||
|
sort: ['configuredProfiles'],
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
label: 'Status',
|
||||||
|
labelKey: 'tableHeaders.status',
|
||||||
|
sort: ['status.status'],
|
||||||
|
value: 'status.status',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return cols;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<div v-else-if="!hasAddonSchema">
|
||||||
|
<Banner color="warning">
|
||||||
|
{{ t('harvester.vgpu.noPermission') }}
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!hasSriovgpuAddon || !hasPCIAddon">
|
||||||
|
<Banner
|
||||||
|
v-if="!hasSriovgpuAddon"
|
||||||
|
color="warning"
|
||||||
|
>
|
||||||
|
<MessageLink
|
||||||
|
:to="toVGpuAddon"
|
||||||
|
prefix-label="harvester.vgpu.goSetting.prefix"
|
||||||
|
middle-label="harvester.vgpu.goSetting.middle"
|
||||||
|
suffix-label="harvester.vgpu.goSetting.suffix"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
<Banner
|
||||||
|
v-if="!hasPCIAddon"
|
||||||
|
color="warning"
|
||||||
|
>
|
||||||
|
<MessageLink
|
||||||
|
:to="toPciAddon"
|
||||||
|
prefix-label="harvester.pci.goSetting.prefix"
|
||||||
|
middle-label="harvester.pci.goSetting.middle"
|
||||||
|
suffix-label="harvester.pci.goSetting.suffix"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="hasSchema">
|
||||||
|
<Banner
|
||||||
|
v-if="showEnableSRIOVGPUMessage"
|
||||||
|
color="warning"
|
||||||
|
>
|
||||||
|
<MessageLink
|
||||||
|
:to="sriovGPUPage"
|
||||||
|
prefix-label="harvester.migconfiguration.goSriovGPU.prefix"
|
||||||
|
middle-label="harvester.migconfiguration.goSriovGPU.middle"
|
||||||
|
suffix-label="harvester.migconfiguration.goSriovGPU.suffix"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
<Banner
|
||||||
|
v-if="!showEnableSRIOVGPUMessage"
|
||||||
|
color="warning"
|
||||||
|
:label="t('harvester.migconfiguration.infoBanner')"
|
||||||
|
/>
|
||||||
|
<ResourceTable
|
||||||
|
v-bind="$attrs"
|
||||||
|
:groupable="false"
|
||||||
|
:namespaced="false"
|
||||||
|
:headers="headers"
|
||||||
|
:schema="schema"
|
||||||
|
:rows="rowsData"
|
||||||
|
key-field="_key"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -83,6 +83,34 @@ export default {
|
|||||||
:rows="rows"
|
:rows="rows"
|
||||||
:schema="schema"
|
:schema="schema"
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
/>
|
>
|
||||||
|
<template #cell:name="{ row }">
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
<router-link
|
||||||
|
v-if="row?.detailLocation"
|
||||||
|
:to="row.detailLocation"
|
||||||
|
>
|
||||||
|
{{ row.nameDisplay }}
|
||||||
|
<i
|
||||||
|
v-if="row.isInternalStorageClass && typeof row.isInternalStorageClass === 'function' ? row.isInternalStorageClass() : false"
|
||||||
|
v-clean-tooltip="t('harvester.storage.internal.cannotDeleteOrDefaultTooltip')"
|
||||||
|
class="icon icon-info text-info"
|
||||||
|
style="margin-left: 0.4em;"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
<span v-else>
|
||||||
|
{{ row.nameDisplay }}
|
||||||
|
<i
|
||||||
|
v-if="row.isInternalStorageClass && typeof row.isInternalStorageClass === 'function' ? row.isInternalStorageClass() : false"
|
||||||
|
v-clean-tooltip="t('harvester.storage.internal.cannotDeleteOrDefaultTooltip')"
|
||||||
|
class="icon icon-info text-info"
|
||||||
|
style="margin-left: 0.4em;"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
</ResourceTable>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Loading from '@shell/components/Loading';
|
|||||||
import MessageLink from '@shell/components/MessageLink';
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
import Masthead from '@shell/components/ResourceList/Masthead';
|
import Masthead from '@shell/components/ResourceList/Masthead';
|
||||||
import ResourceTable from '@shell/components/ResourceTable';
|
import ResourceTable from '@shell/components/ResourceTable';
|
||||||
|
import { isBackupTargetSettingEmpty } from '../utils/setting';
|
||||||
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
|
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
|
||||||
import FilterVMSchedule from '../components/FilterVMSchedule';
|
import FilterVMSchedule from '../components/FilterVMSchedule';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
@@ -39,7 +39,7 @@ export default {
|
|||||||
this.settings = hash.settings;
|
this.settings = hash.settings;
|
||||||
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.SETTING)) {
|
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.SETTING)) {
|
||||||
const backupTargetResource = hash.settings.find( (O) => O.id === 'backup-target');
|
const backupTargetResource = hash.settings.find( (O) => O.id === 'backup-target');
|
||||||
const isEmpty = this.getBackupTargetValueIsEmpty(backupTargetResource);
|
const isEmpty = isBackupTargetSettingEmpty(backupTargetResource);
|
||||||
|
|
||||||
if (backupTargetResource && !isEmpty) {
|
if (backupTargetResource && !isEmpty) {
|
||||||
this.testConnect();
|
this.testConnect();
|
||||||
@@ -78,20 +78,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getBackupTargetValueIsEmpty(resource) {
|
|
||||||
let out = true;
|
|
||||||
|
|
||||||
if (resource?.value) {
|
|
||||||
try {
|
|
||||||
const valueJson = JSON.parse(resource?.value);
|
|
||||||
|
|
||||||
out = !valueJson.type;
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
|
||||||
},
|
|
||||||
|
|
||||||
getRow(row) {
|
getRow(row) {
|
||||||
return row.status && row.status.source;
|
return row.status && row.status.source;
|
||||||
},
|
},
|
||||||
@@ -183,11 +169,9 @@ export default {
|
|||||||
backupTargetResource() {
|
backupTargetResource() {
|
||||||
return this.settings.find((O) => O.id === 'backup-target');
|
return this.settings.find((O) => O.id === 'backup-target');
|
||||||
},
|
},
|
||||||
|
|
||||||
isEmptyValue() {
|
isEmptyValue() {
|
||||||
return this.getBackupTargetValueIsEmpty(this.backupTargetResource);
|
return isBackupTargetSettingEmpty(this.backupTargetResource);
|
||||||
},
|
},
|
||||||
|
|
||||||
canUpdate() {
|
canUpdate() {
|
||||||
return this?.backupTargetResource?.canUpdate;
|
return this?.backupTargetResource?.canUpdate;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import ResourceTable from '@shell/components/ResourceTable';
|
|||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { defaultTableSortGenerationFn } from '@shell/components/ResourceTable.vue';
|
import { defaultTableSortGenerationFn } from '@shell/components/ResourceTable.vue';
|
||||||
import FilterLabel from '../components/FilterLabel';
|
import FilterLabel from '../components/FilterLabel';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '../config/labels-annotations';
|
||||||
|
import { isInternalStorageClass } from '../utils/storage-class';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ListHarvesterImage',
|
name: 'ListHarvesterImage',
|
||||||
@@ -53,6 +55,13 @@ export default {
|
|||||||
|
|
||||||
return base;
|
return base;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isInternalStorageClass(row) {
|
||||||
|
const name = row?.spec?.targetStorageClassName ||
|
||||||
|
row?.metadata?.annotations?.[HCI_ANNOTATIONS.STORAGE_CLASS];
|
||||||
|
|
||||||
|
return isInternalStorageClass(name);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
|
|||||||
import HarvesterVolumeState from '../formatters/HarvesterVolumeState';
|
import HarvesterVolumeState from '../formatters/HarvesterVolumeState';
|
||||||
import { allSettled } from '../utils/promise';
|
import { allSettled } from '../utils/promise';
|
||||||
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
||||||
|
import { INTERNAL_STORAGE_CLASS } from '../config/types';
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
id: HCI.VOLUME,
|
id: HCI.VOLUME,
|
||||||
@@ -130,6 +131,10 @@ export default {
|
|||||||
|
|
||||||
getVMName(row) {
|
getVMName(row) {
|
||||||
return row.attachVM?.metadata?.name || '';
|
return row.attachVM?.metadata?.name || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
isInternalStorageClass(storageClassName) {
|
||||||
|
return this.$store.getters['type-map/labelFor'](INTERNAL_STORAGE_CLASS, storageClassName);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -170,7 +175,7 @@ export default {
|
|||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #col:name="{row}">
|
<template #col:name="{ row }">
|
||||||
<td>
|
<td>
|
||||||
<span>
|
<span>
|
||||||
<router-link
|
<router-link
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ export const VM_HEADERS = [
|
|||||||
{
|
{
|
||||||
name: 'CPU',
|
name: 'CPU',
|
||||||
label: 'CPU',
|
label: 'CPU',
|
||||||
sort: ['spec.template.spec.domain.cpu.cores'],
|
sort: ['displayCPU'],
|
||||||
value: 'spec.template.spec.domain.cpu.cores',
|
value: 'displayCPU',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
dashIfEmpty: true,
|
dashIfEmpty: true,
|
||||||
},
|
},
|
||||||
@@ -74,7 +74,7 @@ export default {
|
|||||||
async fetch() {
|
async fetch() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
const _hash = {
|
const _hash = {
|
||||||
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
|
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM, opt: { force: true } }),
|
||||||
pod: this.$store.dispatch(`${ inStore }/findAll`, { type: POD }),
|
pod: this.$store.dispatch(`${ inStore }/findAll`, { type: POD }),
|
||||||
pvcs: this.$store.dispatch(`${ inStore }/findAll`, { type: PVC }),
|
pvcs: this.$store.dispatch(`${ inStore }/findAll`, { type: PVC }),
|
||||||
pvs: this.$store.dispatch(`${ inStore }/findAll`, { type: PV }),
|
pvs: this.$store.dispatch(`${ inStore }/findAll`, { type: PV }),
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import jsyaml from 'js-yaml';
|
|||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
import difference from 'lodash/difference';
|
import difference from 'lodash/difference';
|
||||||
|
|
||||||
import { sortBy } from '@shell/utils/sort';
|
import { sortBy } from '@shell/utils/sort';
|
||||||
import { set } from '@shell/utils/object';
|
import { set } from '@shell/utils/object';
|
||||||
|
import { getVmCPUMemoryValues } from '../../utils/cpuMemory';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { randomStr } from '@shell/utils/string';
|
import { randomStr } from '@shell/utils/string';
|
||||||
import { base64Decode } from '@shell/utils/crypto';
|
import { base64Decode } from '@shell/utils/crypto';
|
||||||
@@ -143,6 +142,7 @@ export default {
|
|||||||
showYaml: false,
|
showYaml: false,
|
||||||
spec: null,
|
spec: null,
|
||||||
osType: 'linux',
|
osType: 'linux',
|
||||||
|
useTemplate: false,
|
||||||
sshKey: [],
|
sshKey: [],
|
||||||
maintenanceStrategies,
|
maintenanceStrategies,
|
||||||
maintenanceStrategy: 'Migrate',
|
maintenanceStrategy: 'Migrate',
|
||||||
@@ -164,6 +164,9 @@ export default {
|
|||||||
deleteAgent: true,
|
deleteAgent: true,
|
||||||
memory: null,
|
memory: null,
|
||||||
cpu: '',
|
cpu: '',
|
||||||
|
maxMemory: null,
|
||||||
|
maxCpu: '',
|
||||||
|
cpuMemoryHotplugEnabled: false,
|
||||||
reservedMemory: null,
|
reservedMemory: null,
|
||||||
accessCredentials: [],
|
accessCredentials: [],
|
||||||
efiEnabled: false,
|
efiEnabled: false,
|
||||||
@@ -271,7 +274,7 @@ export default {
|
|||||||
|
|
||||||
needNewSecret() {
|
needNewSecret() {
|
||||||
// When creating a template it is always necessary to create a new secret.
|
// When creating a template it is always necessary to create a new secret.
|
||||||
return this.showYaml ? false : this.resourceType === HCI.VM_VERSION || this.isCreate;
|
return this.isCreate || this.showYaml ? false : this.resourceType === HCI.VM_VERSION;
|
||||||
},
|
},
|
||||||
|
|
||||||
defaultTerminationSetting() {
|
defaultTerminationSetting() {
|
||||||
@@ -297,9 +300,11 @@ export default {
|
|||||||
async created() {
|
async created() {
|
||||||
await this.$store.dispatch(`${ this.inStore }/findAll`, { type: SECRET });
|
await this.$store.dispatch(`${ this.inStore }/findAll`, { type: SECRET });
|
||||||
|
|
||||||
if (this.value.vmMachineTypesFeatureEnabled) {
|
if (!this.value.vmMachineTypeAutoFeatureEnabled) {
|
||||||
try {
|
try {
|
||||||
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/clusters/local?link=machineTypes');
|
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl'](
|
||||||
|
'v1/harvester/clusters/local?link=machineTypes'
|
||||||
|
);
|
||||||
const machineTypes = await this.$store.dispatch('harvester/request', { url });
|
const machineTypes = await this.$store.dispatch('harvester/request', { url });
|
||||||
|
|
||||||
this.machineTypes = machineTypes;
|
this.machineTypes = machineTypes;
|
||||||
@@ -348,8 +353,11 @@ export default {
|
|||||||
const runStrategy = spec.runStrategy || 'RerunOnFailure';
|
const runStrategy = spec.runStrategy || 'RerunOnFailure';
|
||||||
const machineType = spec.template.spec.domain?.machine?.type || this.machineTypes[0];
|
const machineType = spec.template.spec.domain?.machine?.type || this.machineTypes[0];
|
||||||
|
|
||||||
const cpu = spec.template.spec.domain?.cpu?.cores;
|
const {
|
||||||
const memory = spec.template.spec.domain.resources.limits.memory;
|
cpu, memory, maxCpu, maxMemory, isHotplugEnabled
|
||||||
|
} = getVmCPUMemoryValues(vm);
|
||||||
|
const cpuMemoryHotplugEnabled = isHotplugEnabled;
|
||||||
|
|
||||||
const reservedMemory = vm.metadata?.annotations?.[HCI_ANNOTATIONS.VM_RESERVED_MEMORY];
|
const reservedMemory = vm.metadata?.annotations?.[HCI_ANNOTATIONS.VM_RESERVED_MEMORY];
|
||||||
const terminationGracePeriodSeconds = spec.template.spec?.terminationGracePeriodSeconds || this.defaultTerminationSetting;
|
const terminationGracePeriodSeconds = spec.template.spec?.terminationGracePeriodSeconds || this.defaultTerminationSetting;
|
||||||
|
|
||||||
@@ -409,6 +417,9 @@ export default {
|
|||||||
|
|
||||||
this['cpu'] = cpu;
|
this['cpu'] = cpu;
|
||||||
this['memory'] = memory;
|
this['memory'] = memory;
|
||||||
|
this['maxCpu'] = maxCpu;
|
||||||
|
this['maxMemory'] = maxMemory;
|
||||||
|
this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
|
||||||
this['reservedMemory'] = reservedMemory;
|
this['reservedMemory'] = reservedMemory;
|
||||||
this['machineType'] = machineType;
|
this['machineType'] = machineType;
|
||||||
this['terminationGracePeriodSeconds'] = terminationGracePeriodSeconds;
|
this['terminationGracePeriodSeconds'] = terminationGracePeriodSeconds;
|
||||||
@@ -633,20 +644,26 @@ export default {
|
|||||||
this.spec.template.spec.domain.machine['type'] = this.machineType;
|
this.spec.template.spec.domain.machine['type'] = this.machineType;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.spec.template.spec.domain.cpu.cores = this.cpu;
|
this.setCPUAndMemory();
|
||||||
this.spec.template.spec.domain.resources.limits.cpu = this.cpu ? this.cpu.toString() : this.cpu;
|
// update terminationGracePeriodSeconds
|
||||||
this.spec.template.spec.domain.resources.limits.memory = this.memory;
|
|
||||||
this.spec.template.spec.terminationGracePeriodSeconds = this.terminationGracePeriodSeconds;
|
this.spec.template.spec.terminationGracePeriodSeconds = this.terminationGracePeriodSeconds;
|
||||||
|
|
||||||
// parse reserved memory
|
|
||||||
const vm = this.resourceType === HCI.VM ? this.value : this.value.spec.vm;
|
const vm = this.resourceType === HCI.VM ? this.value : this.value.spec.vm;
|
||||||
|
|
||||||
|
// parse reserved memory
|
||||||
if (!this.reservedMemory) {
|
if (!this.reservedMemory) {
|
||||||
delete vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY];
|
delete vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY];
|
||||||
} else {
|
} else {
|
||||||
vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY] = this.reservedMemory;
|
vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY] = this.reservedMemory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add or remove cpu memory hotplug annotation
|
||||||
|
if (this.cpuMemoryHotplugEnabled) {
|
||||||
|
vm.metadata.annotations[HCI_ANNOTATIONS.VM_CPU_MEMORY_HOTPLUG] = this.cpuMemoryHotplugEnabled.toString();
|
||||||
|
} else {
|
||||||
|
delete vm.metadata.annotations[HCI_ANNOTATIONS.VM_CPU_MEMORY_HOTPLUG];
|
||||||
|
}
|
||||||
|
|
||||||
if (this.maintenanceStrategy === 'Migrate') {
|
if (this.maintenanceStrategy === 'Migrate') {
|
||||||
delete vm.metadata.labels[HCI_ANNOTATIONS.VM_MAINTENANCE_MODE_STRATEGY];
|
delete vm.metadata.labels[HCI_ANNOTATIONS.VM_MAINTENANCE_MODE_STRATEGY];
|
||||||
} else {
|
} else {
|
||||||
@@ -654,24 +671,45 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setCPUAndMemory() {
|
||||||
|
if (this.cpuMemoryHotplugEnabled) {
|
||||||
|
// set CPU
|
||||||
|
this.spec.template.spec.domain.cpu.sockets = this.cpu;
|
||||||
|
this.spec.template.spec.domain.cpu.cores = 1;
|
||||||
|
|
||||||
|
// set max CPU
|
||||||
|
set(this.spec.template.spec, 'domain.cpu.maxSockets', this.maxCpu);
|
||||||
|
// domain.resources.limits.cpu and memory are defined by k8s which requires string values
|
||||||
|
// see https://kubernetes.io/docs/tasks/configure-pod-container/assign-cpu-resource/
|
||||||
|
set(this.spec.template.spec, 'domain.resources.limits.cpu', this.maxCpu?.toString());
|
||||||
|
|
||||||
|
// set memory
|
||||||
|
set(this.spec.template.spec, 'domain.memory.guest', this.memory);
|
||||||
|
|
||||||
|
// set max memory
|
||||||
|
set(this.spec.template.spec, 'domain.memory.maxGuest', this.maxMemory);
|
||||||
|
set(this.spec.template.spec, 'domain.resources.limits.memory', this.maxMemory);
|
||||||
|
} else {
|
||||||
|
this.spec.template.spec.domain.cpu.sockets = 1;
|
||||||
|
this.spec.template.spec.domain.cpu.cores = this.cpu;
|
||||||
|
this.spec.template.spec.domain.resources.limits.cpu = this.cpu?.toString();
|
||||||
|
this.spec.template.spec.domain.resources.limits.memory = this.memory;
|
||||||
|
// clean below fields as we don't need them if not enable CPU and memory hotplug
|
||||||
|
if (this.spec?.template?.spec?.domain?.memory?.maxGuest) {
|
||||||
|
delete this.spec.template.spec.domain.memory.maxGuest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
parseDiskRows(disk) {
|
parseDiskRows(disk) {
|
||||||
const disks = [];
|
const disks = [];
|
||||||
const volumes = [];
|
const volumes = [];
|
||||||
const diskNameLables = [];
|
const diskNameLabels = [];
|
||||||
const volumeClaimTemplates = [];
|
const volumeClaimTemplates = [];
|
||||||
|
|
||||||
disk.forEach( (R, index) => {
|
disk.forEach( (R, index) => {
|
||||||
const prefixName = this.value.metadata?.name || '';
|
const prefixName = this.value.metadata?.name || '';
|
||||||
|
const dataVolumeName = this.parseDataVolumeName(R, prefixName);
|
||||||
let dataVolumeName = '';
|
|
||||||
|
|
||||||
if (R.source === SOURCE_TYPE.ATTACH_VOLUME) {
|
|
||||||
dataVolumeName = R.volumeName;
|
|
||||||
} else if (this.isClone || !this.hasCreateVolumes.includes(R.realName)) {
|
|
||||||
dataVolumeName = `${ prefixName }-${ R.name }-${ randomStr(5).toLowerCase() }`;
|
|
||||||
} else {
|
|
||||||
dataVolumeName = R.realName;
|
|
||||||
}
|
|
||||||
|
|
||||||
const _disk = this.parseDisk(R, index);
|
const _disk = this.parseDisk(R, index);
|
||||||
const _volume = this.parseVolume(R, dataVolumeName);
|
const _volume = this.parseVolume(R, dataVolumeName);
|
||||||
@@ -679,7 +717,7 @@ export default {
|
|||||||
|
|
||||||
disks.push(_disk);
|
disks.push(_disk);
|
||||||
volumes.push(_volume);
|
volumes.push(_volume);
|
||||||
diskNameLables.push(dataVolumeName);
|
diskNameLabels.push(dataVolumeName);
|
||||||
|
|
||||||
if (R.source !== SOURCE_TYPE.CONTAINER) {
|
if (R.source !== SOURCE_TYPE.CONTAINER) {
|
||||||
volumeClaimTemplates.push(_dataVolumeTemplate);
|
volumeClaimTemplates.push(_dataVolumeTemplate);
|
||||||
@@ -720,6 +758,9 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const specDisks = this.spec?.template?.spec?.domain?.devices?.disks;
|
||||||
|
const mergedDisks = this.mergeDeviceList(specDisks, disks);
|
||||||
|
|
||||||
let spec = {
|
let spec = {
|
||||||
...this.spec,
|
...this.spec,
|
||||||
runStrategy: this.runStrategy,
|
runStrategy: this.runStrategy,
|
||||||
@@ -742,7 +783,7 @@ export default {
|
|||||||
...this.spec.template?.spec?.domain,
|
...this.spec.template?.spec?.domain,
|
||||||
devices: {
|
devices: {
|
||||||
...this.spec.template?.spec?.domain?.devices,
|
...this.spec.template?.spec?.domain?.devices,
|
||||||
disks,
|
disks: mergedDisks,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
volumes,
|
volumes,
|
||||||
@@ -841,13 +882,16 @@ export default {
|
|||||||
interfaces.push(_interface);
|
interfaces.push(_interface);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const specInterfaces = this.spec?.template?.spec?.domain?.devices?.interfaces;
|
||||||
|
const mergedInterfaces = this.mergeInterfaceList(specInterfaces, interfaces);
|
||||||
|
|
||||||
const spec = {
|
const spec = {
|
||||||
...this.spec.template.spec,
|
...this.spec.template.spec,
|
||||||
domain: {
|
domain: {
|
||||||
...this.spec.template.spec.domain,
|
...this.spec.template.spec.domain,
|
||||||
devices: {
|
devices: {
|
||||||
...this.spec.template.spec.domain.devices,
|
...this.spec.template.spec.domain.devices,
|
||||||
interfaces,
|
interfaces: mergedInterfaces,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
networks
|
networks
|
||||||
@@ -952,9 +996,26 @@ export default {
|
|||||||
this['sshKey'] = neu;
|
this['sshKey'] = neu;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateCpuMemory(cpu, memory) {
|
updateCpuMemory(cpu, memory, maxCpu = '', maxMemory = null, cpuMemoryHotplugEnabled = false) {
|
||||||
this['cpu'] = cpu;
|
this['cpu'] = cpu;
|
||||||
this['memory'] = memory;
|
this['memory'] = memory;
|
||||||
|
this['maxCpu'] = maxCpu;
|
||||||
|
this['maxMemory'] = maxMemory;
|
||||||
|
this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
|
||||||
|
},
|
||||||
|
|
||||||
|
parseDataVolumeName(R, prefixName) {
|
||||||
|
let dataVolumeName = '';
|
||||||
|
|
||||||
|
if (R.source === SOURCE_TYPE.ATTACH_VOLUME) {
|
||||||
|
dataVolumeName = R.volumeName;
|
||||||
|
} else if (this.isClone || !this.hasCreateVolumes.includes(R.realName)) {
|
||||||
|
dataVolumeName = `${ prefixName }-${ R.name }-${ randomStr(5).toLowerCase() }`;
|
||||||
|
} else {
|
||||||
|
dataVolumeName = R.realName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataVolumeName;
|
||||||
},
|
},
|
||||||
|
|
||||||
parseDisk(R, index) {
|
parseDisk(R, index) {
|
||||||
@@ -987,7 +1048,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
parseVolumeClaimTemplate(R, dataVolumeName) {
|
parseVolumeClaimTemplate(R, dataVolumeName) {
|
||||||
if (!String(R.size).includes('Gi') && R.size) {
|
const sizeString = String(R.size);
|
||||||
|
|
||||||
|
if (!(sizeString.includes('Gi') || sizeString.includes('Ti')) && R.size) {
|
||||||
R.size = `${ R.size }${ GIBIBYTE }`;
|
R.size = `${ R.size }${ GIBIBYTE }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1478,6 +1541,49 @@ export default {
|
|||||||
this.refreshYamlEditor();
|
this.refreshYamlEditor();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mergeInterfaceList(specInterfaceList, interfaceList) {
|
||||||
|
if (!specInterfaceList || specInterfaceList.length === 0) {
|
||||||
|
return interfaceList;
|
||||||
|
}
|
||||||
|
const specInterfaceMap = new Map(specInterfaceList.map((iface) => [iface.name, iface]));
|
||||||
|
|
||||||
|
return interfaceList.map((iface) => {
|
||||||
|
const specInterface = specInterfaceMap.get(iface.name);
|
||||||
|
|
||||||
|
if (specInterface) {
|
||||||
|
const merged = { ...specInterface, ...iface };
|
||||||
|
|
||||||
|
// currently we only have bridge and masquerade network type, they are mutually exclusive
|
||||||
|
if (iface['bridge']) {
|
||||||
|
delete merged['masquerade'];
|
||||||
|
} else {
|
||||||
|
delete merged['bridge'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
return iface;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
mergeDeviceList(specDeviceList, deviceList) {
|
||||||
|
if (!specDeviceList || specDeviceList.length === 0) {
|
||||||
|
return deviceList;
|
||||||
|
}
|
||||||
|
const specDeviceMap = new Map(specDeviceList.map((device) => [device.name, device]));
|
||||||
|
|
||||||
|
return deviceList.map((device) => {
|
||||||
|
const specDevice = specDeviceMap.get(device.name);
|
||||||
|
|
||||||
|
if (specDevice) {
|
||||||
|
return { ...specDevice, ...device };
|
||||||
|
}
|
||||||
|
|
||||||
|
return device;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
refreshYamlEditor() {
|
refreshYamlEditor() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.yamlEditor?.updateValue();
|
this.$refs.yamlEditor?.updateValue();
|
||||||
@@ -1537,7 +1643,8 @@ export default {
|
|||||||
|
|
||||||
secretRef: {
|
secretRef: {
|
||||||
handler(secret) {
|
handler(secret) {
|
||||||
if (secret && this.resourceType !== HCI.BACKUP) {
|
// we should not inherit the secret if it's from VM template.
|
||||||
|
if (secret && this.resourceType !== HCI.BACKUP && !this.useTemplate) {
|
||||||
this.secretName = secret?.metadata.name;
|
this.secretName = secret?.metadata.name;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { escapeHtml } from '@shell/utils/string';
|
||||||
|
import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
|
||||||
|
import HarvesterResource from './harvester';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing vGPU MIGConfiguration resource.
|
||||||
|
* @extends HarvesterResource
|
||||||
|
*/
|
||||||
|
export default class MIGCONFIGURATION extends HarvesterResource {
|
||||||
|
get _availableActions() {
|
||||||
|
let out = super._availableActions;
|
||||||
|
|
||||||
|
out = out.map((action) => {
|
||||||
|
if (action.action === 'showConfiguration') {
|
||||||
|
return { ...action, enabled: !this.spec.enabled };
|
||||||
|
} else if (action.action === 'goToEditYaml') {
|
||||||
|
return { ...action, enabled: !this.spec.enabled };
|
||||||
|
} else if (action.action === 'goToEdit') {
|
||||||
|
// need to wait for status to be disabled or empty value, then allow user to editConfig
|
||||||
|
return { ...action, enabled: !this.spec.enabled && ['disabled', ''].includes(this.configStatus) };
|
||||||
|
} else {
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
out.push(
|
||||||
|
{
|
||||||
|
action: 'enableConfig',
|
||||||
|
enabled: !this.isEnabled,
|
||||||
|
icon: 'icon icon-fw icon-dot',
|
||||||
|
label: 'Enable',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'disableConfig',
|
||||||
|
enabled: this.isEnabled,
|
||||||
|
icon: 'icon icon-fw icon-dot-open',
|
||||||
|
label: 'Disable',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canYaml() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get disableResourceDetailDrawer() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canDelete() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get configStatus() {
|
||||||
|
return this.status.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
get actualState() {
|
||||||
|
return this.isEnabled ? 'Enabled' : 'Disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
get stateDisplay() {
|
||||||
|
return this.actualState;
|
||||||
|
}
|
||||||
|
|
||||||
|
get stateColor() {
|
||||||
|
const state = this.actualState;
|
||||||
|
|
||||||
|
return colorForState(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isEnabled() {
|
||||||
|
return this.spec.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
get configuredProfiles() {
|
||||||
|
const configuredProfiles = this.spec?.profileSpec?.filter((p) => p.requested > 0) || [];
|
||||||
|
|
||||||
|
if (configuredProfiles.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return configuredProfiles
|
||||||
|
.map((profile) => `${ profile.name } * ${ profile.requested }`)
|
||||||
|
.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
async enableConfig() {
|
||||||
|
try {
|
||||||
|
this.spec.enabled = true;
|
||||||
|
await this.save();
|
||||||
|
} catch (err) {
|
||||||
|
this.$dispatch('growl/fromError', {
|
||||||
|
title: this.t('generic.notification.title.error', { name: escapeHtml(this.name) }),
|
||||||
|
err,
|
||||||
|
}, { root: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async disableConfig() {
|
||||||
|
const { enabled: currentEnabled } = this.spec;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.spec.enabled = false;
|
||||||
|
await this.save();
|
||||||
|
} catch (err) {
|
||||||
|
this.spec.enabled = currentEnabled;
|
||||||
|
this.$dispatch('growl/fromError', {
|
||||||
|
title: this.t('generic.notification.title.error', { name: escapeHtml(this.name) }),
|
||||||
|
err,
|
||||||
|
}, { root: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
import SteveModel from '@shell/plugins/steve/steve-class';
|
import SteveModel from '@shell/plugins/steve/steve-class';
|
||||||
import { VIEW_IN_API, DEV } from '@shell/store/prefs';
|
import { VIEW_IN_API, DEV } from '@shell/store/prefs';
|
||||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
||||||
|
import { HARVESTER_CRD_MAP } from '../config/harvester-map';
|
||||||
|
|
||||||
export default class HarvesterResource extends SteveModel {
|
export default class HarvesterResource extends SteveModel {
|
||||||
get listLocation() {
|
get listLocation() {
|
||||||
const name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
const name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||||
|
const resource = HARVESTER_CRD_MAP[this.type] || this.type;
|
||||||
|
|
||||||
return this.$rootGetters['type-map/optionsFor'](this.type).customRoute || {
|
return this.$rootGetters['type-map/optionsFor'](this.type).customRoute || {
|
||||||
name,
|
name,
|
||||||
params: {
|
params: {
|
||||||
product: this.$rootGetters['productId'],
|
product: this.$rootGetters['productId'],
|
||||||
cluster: this.$rootGetters['clusterId'],
|
cluster: this.$rootGetters['clusterId'],
|
||||||
resource: this.type,
|
resource
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// This file is Cloud Configuration Templates page model
|
||||||
import { clone } from '@shell/utils/object';
|
import { clone } from '@shell/utils/object';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
import HarvesterResource from '../harvester';
|
import HarvesterResource from '../harvester';
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default class HarvesterNetworkAttachmentDef extends NetworkAttachmentDef
|
|||||||
params: {
|
params: {
|
||||||
product: HARVESTER_PRODUCT,
|
product: HARVESTER_PRODUCT,
|
||||||
cluster: this.$rootGetters['clusterId'],
|
cluster: this.$rootGetters['clusterId'],
|
||||||
resource: this.type,
|
resource: HCI.NETWORK_ATTACHMENT,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,19 @@ export default class HciClusterflow extends HarvesterFlow {
|
|||||||
return this.$rootGetters['harvester/all'](LOGGING.CLUSTER_OUTPUT) || [];
|
return this.$rootGetters['harvester/all'](LOGGING.CLUSTER_OUTPUT) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get listLocation() {
|
||||||
|
const listLocation = clone(super.listLocation);
|
||||||
|
|
||||||
|
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||||
|
listLocation.params.resource = HCI.CLUSTER_FLOW;
|
||||||
|
|
||||||
|
return listLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
get harvesterResourcesInExplorer() {
|
||||||
|
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||||
|
}
|
||||||
|
|
||||||
get _detailLocation() {
|
get _detailLocation() {
|
||||||
const schema = this.$getters['schemaFor'](this.type);
|
const schema = this.$getters['schemaFor'](this.type);
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,19 @@ export default class HciClusteroutput extends LogOutput {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get listLocation() {
|
||||||
|
const listLocation = clone(super.listLocation);
|
||||||
|
|
||||||
|
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||||
|
listLocation.params.resource = HCI.CLUSTER_OUTPUT;
|
||||||
|
|
||||||
|
return listLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
get harvesterResourcesInExplorer() {
|
||||||
|
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||||
|
}
|
||||||
|
|
||||||
get detailLocation() {
|
get detailLocation() {
|
||||||
const detailLocation = clone(this._detailLocation);
|
const detailLocation = clone(this._detailLocation);
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,19 @@ export default class HarvesterLogFlow extends Flow {
|
|||||||
return this.$rootGetters['harvester/all'](LOGGING.CLUSTER_OUTPUT) || [];
|
return this.$rootGetters['harvester/all'](LOGGING.CLUSTER_OUTPUT) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get listLocation() {
|
||||||
|
const listLocation = clone(super.listLocation);
|
||||||
|
|
||||||
|
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||||
|
listLocation.params.resource = HCI.FLOW;
|
||||||
|
|
||||||
|
return listLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
get harvesterResourcesInExplorer() {
|
||||||
|
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||||
|
}
|
||||||
|
|
||||||
get _detailLocation() {
|
get _detailLocation() {
|
||||||
const schema = this.$getters['schemaFor'](this.type);
|
const schema = this.$getters['schemaFor'](this.type);
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,19 @@ export default class HarvesterLogOutput extends LogOutput {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get listLocation() {
|
||||||
|
const listLocation = clone(super.listLocation);
|
||||||
|
|
||||||
|
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||||
|
listLocation.params.resource = HCI.OUTPUT;
|
||||||
|
|
||||||
|
return listLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
get harvesterResourcesInExplorer() {
|
||||||
|
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||||
|
}
|
||||||
|
|
||||||
get detailLocation() {
|
get detailLocation() {
|
||||||
const detailLocation = clone(this._detailLocation);
|
const detailLocation = clone(this._detailLocation);
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,19 @@ export default class HciAlertmanagerConfig extends SteveModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get listLocation() {
|
||||||
|
const listLocation = clone(super.listLocation);
|
||||||
|
|
||||||
|
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||||
|
listLocation.params.resource = HCI.ALERTMANAGERCONFIG;
|
||||||
|
|
||||||
|
return listLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
get harvesterResourcesInExplorer() {
|
||||||
|
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||||
|
}
|
||||||
|
|
||||||
get doneOverride() {
|
get doneOverride() {
|
||||||
const detailLocation = clone(this._detailLocation);
|
const detailLocation = clone(this._detailLocation);
|
||||||
|
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ export default class HciNode extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isCordoned() {
|
get isCordoned() {
|
||||||
return (this.isUnSchedulable && !this.isEtcd) || this.hasAction('uncordon');
|
return this.hasAction('uncordon');
|
||||||
}
|
}
|
||||||
|
|
||||||
get isEtcd() {
|
get isEtcd() {
|
||||||
|
|||||||
@@ -35,15 +35,10 @@ export default class HciPv extends HarvesterResource {
|
|||||||
get availableActions() {
|
get availableActions() {
|
||||||
let out = super._availableActions;
|
let out = super._availableActions;
|
||||||
|
|
||||||
// Longhorn V2 provisioner do not support volume clone feature yet
|
const clone = out.find((action) => action.action === 'goToClone');
|
||||||
if (this.isLonghornV2) {
|
|
||||||
out = out.filter((action) => action.action !== 'goToClone');
|
|
||||||
} else {
|
|
||||||
const clone = out.find((action) => action.action === 'goToClone');
|
|
||||||
|
|
||||||
if (clone) {
|
if (clone) {
|
||||||
clone.action = 'goToCloneVolume';
|
clone.action = 'goToCloneVolume';
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportImageAction = {
|
const exportImageAction = {
|
||||||
@@ -65,10 +60,6 @@ export default class HciPv extends HarvesterResource {
|
|||||||
takeSnapshotAction,
|
takeSnapshotAction,
|
||||||
...out
|
...out
|
||||||
];
|
];
|
||||||
// TODO: remove this block if Longhorn V2 engine supports restore volume snapshot
|
|
||||||
if (this.isLonghornV2) {
|
|
||||||
out = out.filter((action) => action.action !== takeSnapshotAction.action);
|
|
||||||
}
|
|
||||||
} else { // v1.4 / v1.3
|
} else { // v1.4 / v1.3
|
||||||
if (!this.isLonghorn || !this.isLonghornV2) {
|
if (!this.isLonghorn || !this.isLonghornV2) {
|
||||||
out = [
|
out = [
|
||||||
|
|||||||
@@ -5,6 +5,25 @@ import Secret from '@shell/models/secret';
|
|||||||
import { NAMESPACE } from '@shell/config/types';
|
import { NAMESPACE } from '@shell/config/types';
|
||||||
|
|
||||||
export default class HciSecret extends Secret {
|
export default class HciSecret extends Secret {
|
||||||
|
// prevent harvester secret detail page be overridden.
|
||||||
|
// See isFullPageOverride in https://github.com/rancher/dashboard/blob/master/shell/components/ResourceDetail/index.vue
|
||||||
|
get fullDetailPageOverride() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get listLocation() {
|
||||||
|
const listLocation = clone(super.listLocation);
|
||||||
|
|
||||||
|
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||||
|
listLocation.params.resource = HCI.SECRET;
|
||||||
|
|
||||||
|
return listLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
get harvesterResourcesInExplorer() {
|
||||||
|
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||||
|
}
|
||||||
|
|
||||||
get _detailLocation() {
|
get _detailLocation() {
|
||||||
const schema = this.$getters['schemaFor'](this.type);
|
const schema = this.$getters['schemaFor'](this.type);
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,24 @@ import { HCI } from '../../types';
|
|||||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
|
||||||
import { LONGHORN_DRIVER } from '@shell/config/types';
|
import { LONGHORN_DRIVER } from '@shell/config/types';
|
||||||
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
|
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
|
||||||
|
import { isInternalStorageClass } from '../../utils/storage-class';
|
||||||
|
|
||||||
export const LVM_DRIVER = 'lvm.driver.harvesterhci.io';
|
export const LVM_DRIVER = 'lvm.driver.harvesterhci.io';
|
||||||
|
|
||||||
export default class HciStorageClass extends StorageClass {
|
export default class HciStorageClass extends StorageClass {
|
||||||
|
get listLocation() {
|
||||||
|
const listLocation = clone(super.listLocation);
|
||||||
|
|
||||||
|
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
||||||
|
listLocation.params.resource = HCI.STORAGE;
|
||||||
|
|
||||||
|
return listLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
get harvesterResourcesInExplorer() {
|
||||||
|
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
|
||||||
|
}
|
||||||
|
|
||||||
get detailLocation() {
|
get detailLocation() {
|
||||||
const detailLocation = clone(this._detailLocation);
|
const detailLocation = clone(this._detailLocation);
|
||||||
|
|
||||||
@@ -85,4 +99,23 @@ export default class HciStorageClass extends StorageClass {
|
|||||||
get thirdPartyStorageFeatureEnabled() {
|
get thirdPartyStorageFeatureEnabled() {
|
||||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isInternalStorageClass() {
|
||||||
|
return isInternalStorageClass(this.metadata?.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
get availableActions() {
|
||||||
|
let out = super.availableActions || [];
|
||||||
|
|
||||||
|
if (this.isInternalStorageClass()) {
|
||||||
|
out = out.filter((action) => {
|
||||||
|
return !['setDefault', 'setAsDefault', 'promptRemove'].includes(action.action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (out[0] && out[0].divider === true) {
|
||||||
|
out = out.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,8 +113,9 @@ export default class HciAddonConfig extends HarvesterResource {
|
|||||||
|
|
||||||
get displayName() {
|
get displayName() {
|
||||||
const isExperimental = this.metadata?.labels?.[HCI_ANNOTATIONS.ADDON_EXPERIMENTAL] === 'true';
|
const isExperimental = this.metadata?.labels?.[HCI_ANNOTATIONS.ADDON_EXPERIMENTAL] === 'true';
|
||||||
|
const name = this.metadata?.labels?.[HCI_ANNOTATIONS.ADDON_DISPLAYNAME] || this.metadata.name;
|
||||||
|
|
||||||
return isExperimental ? `${ this.metadata.name } (${ this.t('generic.experimental') })` : this.metadata.name;
|
return isExperimental ? `${ name } (${ this.t('generic.experimental') })` : name;
|
||||||
}
|
}
|
||||||
|
|
||||||
get customValidationRules() {
|
get customValidationRules() {
|
||||||
|
|||||||
@@ -52,11 +52,19 @@ export default class HciSetting extends HarvesterResource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get clusterRegistrationTLSVerifyFeatureEnabled() {
|
||||||
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('clusterRegistrationTLSVerify');
|
||||||
|
}
|
||||||
|
|
||||||
get customValue() {
|
get customValue() {
|
||||||
if (this.metadata.name === HCI_SETTING.STORAGE_NETWORK) {
|
if (this.metadata.name === HCI_SETTING.STORAGE_NETWORK) {
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(JSON.parse(this.value), null, 2);
|
return JSON.stringify(JSON.parse(this.value), null, 2);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
} else if (this.metadata.name === HCI_SETTING.CLUSTER_REGISTRATION_URL) {
|
||||||
|
try {
|
||||||
|
return this.clusterRegistrationTLSVerifyFeatureEnabled ? JSON.stringify(JSON.parse(this.value), null, 2) : this.value;
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -156,4 +164,8 @@ export default class HciSetting extends HarvesterResource {
|
|||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get disableResourceDetailDrawer() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user