mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-04 21:26:08 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19b6842b21 | ||
|
|
fa78229d3a | ||
|
|
c3bfaf00ea | ||
|
|
1a176960bc | ||
|
|
e82ba9d9f5 | ||
|
|
f7fc001abd | ||
|
|
d1c676ee2c | ||
|
|
c562dd73ba | ||
|
|
1cd5e5ca7d | ||
|
|
1aaa8f71c4 | ||
|
|
003a09d0a9 | ||
|
|
f08e632aad | ||
|
|
df1d7001bc | ||
|
|
41f78c83a7 | ||
|
|
f1a53311ce | ||
|
|
e0c6e905a9 | ||
|
|
1852e434f1 | ||
|
|
77c085d3a7 | ||
|
|
9dbe7d9015 | ||
|
|
3f19494d98 | ||
|
|
296601d38c | ||
|
|
5d9ba584b8 | ||
|
|
eab4ba3d0f | ||
|
|
4e5320dc72 | ||
|
|
ba7092f400 | ||
|
|
eec7c3573d | ||
|
|
f0014cc627 | ||
|
|
9aa311a7e3 | ||
|
|
013d9f1d7e | ||
|
|
8c044ba44c | ||
|
|
7b8592e07d | ||
|
|
53d2e67b4f | ||
|
|
1d146a40bd | ||
|
|
11d6943596 | ||
|
|
7b754457ec | ||
|
|
2cb838eded | ||
|
|
bdcb34ab14 | ||
|
|
8daf44b3f6 | ||
|
|
8044e74102 | ||
|
|
264d65cc6d | ||
|
|
342a82c8e0 | ||
|
|
e7cd5cc4b0 | ||
|
|
cf405be71e | ||
|
|
911d30d7ba | ||
|
|
1b4bfa0962 | ||
|
|
d0f6f18a8a |
@@ -14,4 +14,3 @@ 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@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
|
|||||||
+31
-12
@@ -1,14 +1,33 @@
|
|||||||
pull_request_rules:
|
pull_request_rules:
|
||||||
- name: Ask to resolve conflict
|
- name: Automatically open backport PR to release-harvester-v1.5
|
||||||
conditions:
|
conditions:
|
||||||
- conflict
|
- base=main
|
||||||
actions:
|
- label="require backport 1.5"
|
||||||
comment:
|
actions:
|
||||||
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
|
backport:
|
||||||
|
branches:
|
||||||
|
- "release-harvester-v1.5"
|
||||||
|
assignees:
|
||||||
|
- "{{ author }}"
|
||||||
|
labels:
|
||||||
|
- "v1.5 backport PR"
|
||||||
|
|
||||||
commands_restrictions:
|
- name: Automatically open backport PR to release-harvester-v1.0
|
||||||
backport:
|
conditions:
|
||||||
conditions:
|
- base=main
|
||||||
- or:
|
- label="require backport 1.0"
|
||||||
- sender-permission>=write
|
actions:
|
||||||
- sender=github-actions[bot]
|
backport:
|
||||||
|
branches:
|
||||||
|
- "release-harvester-v1.0"
|
||||||
|
assignees:
|
||||||
|
- "{{ author }}"
|
||||||
|
labels:
|
||||||
|
- "v1.0 backport PR"
|
||||||
|
|
||||||
|
- name: Ask to resolve conflict
|
||||||
|
conditions:
|
||||||
|
- conflict
|
||||||
|
actions:
|
||||||
|
comment:
|
||||||
|
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
# .github/release.yml
|
|
||||||
changelog:
|
|
||||||
categories:
|
|
||||||
- title: Features
|
|
||||||
labels:
|
|
||||||
- feat
|
|
||||||
- title: Bug Fixes
|
|
||||||
labels:
|
|
||||||
- fix
|
|
||||||
- title: Dependencies
|
|
||||||
labels:
|
|
||||||
- deps
|
|
||||||
- title: Performance
|
|
||||||
labels:
|
|
||||||
- perf
|
|
||||||
- title: Documentation
|
|
||||||
labels:
|
|
||||||
- docs
|
|
||||||
- title: Style
|
|
||||||
labels:
|
|
||||||
- style
|
|
||||||
- title: Build & Continuous Integration
|
|
||||||
labels:
|
|
||||||
- build
|
|
||||||
- ci
|
|
||||||
- title: Security
|
|
||||||
labels:
|
|
||||||
- security
|
|
||||||
- title: Tests
|
|
||||||
labels:
|
|
||||||
- test
|
|
||||||
- title: Refactoring
|
|
||||||
labels:
|
|
||||||
- refactor
|
|
||||||
- title: Chores
|
|
||||||
labels:
|
|
||||||
- chore
|
|
||||||
- title: Others
|
|
||||||
labels:
|
|
||||||
- other
|
|
||||||
+5
-10
@@ -1,18 +1,15 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": [
|
"extends": ["config:recommended"],
|
||||||
"config:recommended",
|
|
||||||
":semanticCommitTypeAll(deps)",
|
|
||||||
":semanticCommitScopeDisabled"
|
|
||||||
],
|
|
||||||
"baseBranches": [
|
"baseBranches": [
|
||||||
|
"master",
|
||||||
"main",
|
"main",
|
||||||
"release-harvester-v1.7",
|
"/^release-harvester-v\\d+\\.\\d+$/"
|
||||||
"release-harvester-v1.8"
|
|
||||||
],
|
],
|
||||||
"automergeMajor": false,
|
"automergeMajor": false,
|
||||||
"semanticCommits": "enabled",
|
"semanticCommits": "enabled",
|
||||||
"semanticCommitType": "deps",
|
"semanticCommitType": "chore",
|
||||||
|
"semanticCommitScope": "deps",
|
||||||
"prHourlyLimit": 12,
|
"prHourlyLimit": 12,
|
||||||
"timezone": "Asia/Taipei",
|
"timezone": "Asia/Taipei",
|
||||||
"schedule": ["after 10am on sunday"],
|
"schedule": ["after 10am on sunday"],
|
||||||
@@ -34,14 +31,12 @@
|
|||||||
{
|
{
|
||||||
"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@b70f0bdf12a03e5e3f33e4f92ccb6c89deb3ebd9 # main
|
- uses: rancher/gh-issue-mgr/auto-assign-action@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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.base_ref }}
|
ref: ${{ github.base_ref }}
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
name: "[PR Management] Request Backport via Mergify"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [closed]
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
comment-backport:
|
|
||||||
if: github.event.pull_request.merged == true
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.base_ref }}
|
|
||||||
|
|
||||||
- name: Post Mergify backport command
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
pr_number=${{ github.event.pull_request.number }}
|
|
||||||
repo="${{ github.repository }}"
|
|
||||||
|
|
||||||
labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
|
|
||||||
labels=$(echo "$labels_json" | jq -r '.[] // empty')
|
|
||||||
|
|
||||||
echo "Labels on PR: $labels"
|
|
||||||
|
|
||||||
matches=$(echo "$labels" | grep -oE '^require backport/v[0-9]+\.[0-9]+$' || true)
|
|
||||||
|
|
||||||
if [[ -z "$matches" ]]; then
|
|
||||||
echo "No back‑port labels found — skipping."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
branches=$(echo "$matches" \
|
|
||||||
| sed -E 's/^require backport\/v/release-harvester-v/' \
|
|
||||||
| sort -u | tr '\n' ' ')
|
|
||||||
branches=${branches%% }
|
|
||||||
|
|
||||||
cmd="@Mergifyio backport $branches"
|
|
||||||
echo "Posting Mergify command: $cmd"
|
|
||||||
gh pr comment "$pr_number" --repo "$repo" --body "$cmd"
|
|
||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@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@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
- uses: actions/checkout@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@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@@ -45,20 +45,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@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
uses: rancher-eio/read-vault-secrets@main
|
||||||
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@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
uses: 'google-github-actions/auth@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@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
uses: 'google-github-actions/upload-cloud-storage@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}}
|
||||||
@@ -72,12 +71,12 @@ jobs:
|
|||||||
name: Build & Upload Embedded
|
name: Build & Upload Embedded
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
- uses: actions/checkout@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@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@@ -90,19 +89,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@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
uses: rancher-eio/read-vault-secrets@main
|
||||||
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@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
uses: 'google-github-actions/auth@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@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
uses: 'google-github-actions/upload-cloud-storage@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
|
||||||
@@ -115,12 +114,12 @@ jobs:
|
|||||||
name: Build & Upload Plugin
|
name: Build & Upload Plugin
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
- uses: actions/checkout@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@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@@ -134,19 +133,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@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
uses: rancher-eio/read-vault-secrets@main
|
||||||
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@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
uses: 'google-github-actions/auth@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@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
uses: 'google-github-actions/upload-cloud-storage@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
|
||||||
@@ -156,7 +155,7 @@ jobs:
|
|||||||
process_gcloudignore: false
|
process_gcloudignore: false
|
||||||
|
|
||||||
- name: Upload plugin directory
|
- name: Upload plugin directory
|
||||||
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
uses: 'google-github-actions/upload-cloud-storage@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@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
uses: rancher-eio/read-vault-secrets@main
|
||||||
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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@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@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
uses: docker/login-action@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@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
|
uses: azure/setup-helm@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@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
uses: actions/setup-node@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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@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@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
|
uses: azure/setup-helm@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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
uses: actions/upload-artifact@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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@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@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
uses: actions/download-artifact@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@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0
|
uses: helm/chart-releaser-action@v1.4.1
|
||||||
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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@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@3b26a36bad555e5e2b8634b24823be29732f287c # master
|
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
|
||||||
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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@v4
|
||||||
- name: Check package version
|
- name: Check package version
|
||||||
env:
|
env:
|
||||||
TAG_VERSION: ${{github.ref_name}}
|
TAG_VERSION: ${{github.ref_name}}
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
name: FOSSA Scanning
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["main", "release-harvester-v*"]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
fossa-scanning:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 30
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
|
|
||||||
# The FOSSA token is shared between all repos in Harvester's GH org. It can
|
|
||||||
# be used directly and there is no need to request specific access to EIO.
|
|
||||||
- name: Read FOSSA token
|
|
||||||
uses: rancher-eio/read-vault-secrets@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
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
name: "[PR Management] Add PR Label"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened, reopened, edited]
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- 'release-harvester-v*'
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
auto-assign-pr-label:
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.base_ref }}
|
|
||||||
- name: Setup Nodejs and yarn install
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
- name: Set PR label
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
|
|
||||||
echo "PR_LABEL = $PR_LABEL"
|
|
||||||
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "$PR_LABEL"
|
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
name: Tests
|
name: Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call: # This tells GH that the workflow is reusable
|
workflow_call: # This tells GH that the workflow is reusable
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- 'release-*'
|
- 'release-*'
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, reopened, edited, synchronize]
|
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- 'release-*'
|
- 'release-*'
|
||||||
@@ -16,18 +15,13 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
- uses: actions/checkout@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: |
|
||||||
@@ -41,14 +35,8 @@ 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
|
||||||
if [ "$GITHUB_BEFORE" = "0000000000000000000000000000000000000000" ]; then
|
FROM="$GITHUB_BEFORE"
|
||||||
# first push to HEAD
|
TO="$GITHUB_AFTER"
|
||||||
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
|
||||||
@@ -56,15 +44,7 @@ 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 }}
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
@a110605
|
|
||||||
@houhoucoop
|
|
||||||
@@ -7,7 +7,7 @@ The Harvester UI Extension is a Rancher extension that provides the user interfa
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
|
For detailed installation instructions, please refer to the [official Harvester documentation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210).
|
||||||
|
|
||||||
|
|
||||||
## Development Setup
|
## Development Setup
|
||||||
@@ -138,6 +138,7 @@ 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**
|
||||||
@@ -163,7 +164,7 @@ If you want to contribute, start by reading this document, then visit our [Getti
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (c) 2014-2026 [SUSE, LLC.](https://www.suse.com/)
|
Copyright (c) 2014-2025 [SUSE, LLC.](https://www.suse.com/)
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -22,10 +22,6 @@ 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'],
|
||||||
|
|||||||
+11
-18
@@ -1,41 +1,36 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.8.0",
|
"version": "1.6.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-class-static-block": "7.28.6",
|
"@babel/plugin-transform-class-static-block": "7.26.0",
|
||||||
"@rancher/shell": "3.0.9-rc.6",
|
"@rancher/shell": "3.0.5-rc.7",
|
||||||
"@vue-flow/background": "^1.3.0",
|
|
||||||
"@vue-flow/controls": "^1.1.1",
|
|
||||||
"@vue-flow/core": "^1.33.5",
|
|
||||||
"@vue-flow/minimap": "^1.4.0",
|
|
||||||
"cache-loader": "^4.1.0",
|
"cache-loader": "^4.1.0",
|
||||||
"color": "4.2.3",
|
"color": "4.2.3",
|
||||||
"ip": "2.0.1",
|
"ip": "2.0.1",
|
||||||
"node-polyfill-webpack-plugin": "^3.0.0",
|
"node-polyfill-webpack-plugin": "^3.0.0",
|
||||||
"elkjs": "^0.11.0",
|
|
||||||
"vue-draggable-next": "^2.2.1",
|
"vue-draggable-next": "^2.2.1",
|
||||||
"yaml": "^2.5.1"
|
"yaml": "^2.5.1"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/node": "~20.19.0",
|
"@types/node": "~20.10.0",
|
||||||
"cronstrue": "2.59.0",
|
"cronstrue": "2.50.0",
|
||||||
"d3-color": "3.1.0",
|
"d3-color": "3.1.0",
|
||||||
"ejs": "3.1.10",
|
"ejs": "3.1.10",
|
||||||
"follow-redirects": "1.15.11",
|
"follow-redirects": "1.15.6",
|
||||||
"glob": "7.2.3",
|
"glob": "7.2.3",
|
||||||
"glob-parent": "6.0.2",
|
"glob-parent": "6.0.2",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"@types/lodash": "4.17.24",
|
"@types/lodash": "4.17.5",
|
||||||
"merge": "2.1.1",
|
"merge": "2.1.1",
|
||||||
"node-forge": "1.3.3",
|
"node-forge": "1.3.1",
|
||||||
"nth-check": "2.1.1",
|
"nth-check": "2.1.1",
|
||||||
"qs": "6.15.0",
|
"qs": "6.11.1",
|
||||||
"roarr": "7.21.4",
|
"roarr": "7.0.4",
|
||||||
"semver": "7.7.4",
|
"semver": "7.5.4",
|
||||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -43,7 +38,6 @@
|
|||||||
"build": "./node_modules/.bin/vue-cli-service build",
|
"build": "./node_modules/.bin/vue-cli-service build",
|
||||||
"clean": "./node_modules/@rancher/shell/scripts/clean",
|
"clean": "./node_modules/@rancher/shell/scripts/clean",
|
||||||
"lint": "./node_modules/.bin/eslint --max-warnings 0 --ext .js,.ts,.vue .",
|
"lint": "./node_modules/.bin/eslint --max-warnings 0 --ext .js,.ts,.vue .",
|
||||||
"lint:fix": "./node_modules/.bin/eslint --fix --max-warnings 0 --ext .js,.ts,.vue .",
|
|
||||||
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
|
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
|
||||||
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
|
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
|
||||||
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
|
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
|
||||||
@@ -52,7 +46,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
# harvester-ui-extension
|
|
||||||
|
|
||||||
The Harvester UI Extension is a Rancher extension that provides the user interface for [Harvester](https://harvesterhci.io) within the [Rancher Dashboard](https://github.com/rancher/dashboard).
|
|
||||||
|
|
||||||
> **Note:**
|
|
||||||
> This extension is available starting from **Rancher 2.10.0**. Ensure your Rancher version is **2.10.0 or later** to access Harvester integration.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
|
|
||||||
|
|
||||||
## Development Setup
|
|
||||||
|
|
||||||
Ensure **Node.js v20 or later** is installed for development and debugging.
|
|
||||||
|
|
||||||
### Standalone Mode
|
|
||||||
|
|
||||||
Run the extension standalone with hot reload at `https://localhost:8005`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install dependencies
|
|
||||||
yarn install
|
|
||||||
|
|
||||||
# Start the development server
|
|
||||||
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
|
||||||
|
|
||||||
# Example with specific server version
|
|
||||||
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.5.0 API=https://192.168.1.123 yarn dev
|
|
||||||
```
|
|
||||||
|
|
||||||
You may also define environment variables in a `.env` file:
|
|
||||||
|
|
||||||
```env
|
|
||||||
RANCHER_ENV=harvester
|
|
||||||
VUE_APP_SERVER_VERSION=v1.5.0
|
|
||||||
API=https://192.168.1.123
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rancher Integration Mode
|
|
||||||
|
|
||||||
To run as a Rancher extension, follow the [Rancher UI Extension Guide](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
API=https://your-rancher-ip yarn dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Commit Message Guidelines
|
|
||||||
|
|
||||||
This project uses [commit-lint](https://commitlint.js.org/) with [Conventional Commits](https://www.conventionalcommits.org/) to ensure consistent and meaningful commit messages.
|
|
||||||
|
|
||||||
### Commit Message Format
|
|
||||||
|
|
||||||
All commit messages must follow the conventional commit format:
|
|
||||||
|
|
||||||
```
|
|
||||||
<type>[optional scope]: <description>
|
|
||||||
|
|
||||||
[optional body]
|
|
||||||
|
|
||||||
[optional footer(s)]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Supported Types
|
|
||||||
|
|
||||||
- **feat**: New features
|
|
||||||
- **fix**: Bug fixes
|
|
||||||
- **docs**: Documentation changes
|
|
||||||
- **style**: Code style changes (formatting, missing semicolons, etc.)
|
|
||||||
- **refactor**: Code refactoring
|
|
||||||
- **perf**: Performance improvements
|
|
||||||
- **test**: Adding or updating tests
|
|
||||||
- **build**: Build system or external dependencies
|
|
||||||
- **ci**: CI/CD changes
|
|
||||||
- **chore**: Other changes that don't modify src or test files
|
|
||||||
- **revert**: Reverts a previous commit
|
|
||||||
- **wip**: Work in progress
|
|
||||||
- **deps**: Dependency updates
|
|
||||||
- **security**: Security fixes
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Feature
|
|
||||||
git commit -m "feat: add new virtual machine creation wizard"
|
|
||||||
|
|
||||||
# Bug fix
|
|
||||||
git commit -m "fix: resolve memory leak in VM console"
|
|
||||||
|
|
||||||
# Documentation
|
|
||||||
git commit -m "docs: update installation instructions"
|
|
||||||
|
|
||||||
# Breaking change
|
|
||||||
git commit -m "feat!: change API endpoint structure
|
|
||||||
|
|
||||||
BREAKING CHANGE: The /api/v1/vms endpoint has been replaced with /api/v2/vms"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Git Hooks
|
|
||||||
|
|
||||||
The project uses [Husky](https://typicode.github.io/husky/) to automatically validate commit messages and run linting before commits:
|
|
||||||
|
|
||||||
- **pre-commit**: Runs ESLint to ensure code quality
|
|
||||||
- **commit-msg**: Validates commit message format using commit-lint
|
|
||||||
|
|
||||||
These hooks are automatically installed when you run `yarn install`.
|
|
||||||
|
|
||||||
### Manual Validation
|
|
||||||
|
|
||||||
You can manually validate commit messages:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Validate the last commit
|
|
||||||
yarn commitlint
|
|
||||||
|
|
||||||
# Validate a specific commit
|
|
||||||
npx commitlint --from <commit-hash>
|
|
||||||
|
|
||||||
# Validate a range of commits
|
|
||||||
npx commitlint --from <start-hash> --to <end-hash>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Branch Structure
|
|
||||||
|
|
||||||
- **`main`** – Main development branch
|
|
||||||
- **`release-harvester-vX.Y`** – Stable release branches per version series
|
|
||||||
- **`vX.Y-head`** – Testing branches for ongoing changes to extension builds in each release series
|
|
||||||
|
|
||||||
> **Note:**
|
|
||||||
> The `vX.Y-head` branches are auto-generated and kept in sync with release branches. Use these for testing the latest changes in each version series.
|
|
||||||
|
|
||||||
## Testing Guidelines
|
|
||||||
|
|
||||||
### UI Extension Testing
|
|
||||||
|
|
||||||
To validate changes in a release series, switch to the appropriate `vX.Y-head` branch. For main branch testing, use `main-head`.
|
|
||||||
|
|
||||||
- Examples:
|
|
||||||
- Test `1.0.x` series → `v1.0-head`
|
|
||||||
- Test `1.5.x` series → `v1.5-head`
|
|
||||||
|
|
||||||
**Steps:**
|
|
||||||
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
|
|
||||||
2. Refresh the Harvester repository using the target `vX.Y-head` branch
|
|
||||||
3. Go to the **Extensions** page and install the desired version
|
|
||||||
|
|
||||||
### Standalone Mode Testing
|
|
||||||
|
|
||||||
To test the standalone UI, configure Harvester to load the UI from an external source.
|
|
||||||
|
|
||||||
- Examples of `ui-index`:
|
|
||||||
- Main branch → `https://releases.rancher.com/harvester-ui/dashboard/latest/index.html`
|
|
||||||
- Release series `1.5.x` → `https://releases.rancher.com/harvester-ui/dashboard/release-harvester-v1.5/index.html`
|
|
||||||
|
|
||||||
**Steps:**
|
|
||||||
1. Go to **Harvester UI** → **Advanced** → **Settings** → **UI**
|
|
||||||
2. Set **ui-source** to `External`
|
|
||||||
3. Set **ui-index** to the desired URL
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
If you want to contribute, start by reading this document, then visit our [Getting Started guide](https://extensions.rancher.io/extensions/next/extensions-getting-started) to learn how to develop and submit changes.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Copyright (c) 2014-2026 [SUSE, LLC.](https://www.suse.com/)
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import Collapse from '@shell/components/Collapse';
|
import Collapse from '@shell/components/Collapse';
|
||||||
import PercentageBar from '@shell/components/PercentageBar';
|
import PercentageBar from '@shell/components/PercentageBar';
|
||||||
import { HCI } from '../types';
|
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterUpgradeProgressList',
|
name: 'HarvesterUpgradeProgressList',
|
||||||
@@ -27,45 +25,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
await this.$store.dispatch('harvester/findAll', { type: HCI.UPGRADE });
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return { open: true };
|
return { open: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
|
||||||
showResumeButton() {
|
|
||||||
return this.title === 'Upgrading Node';
|
|
||||||
},
|
|
||||||
latestUpgradeCR() {
|
|
||||||
return this.$store.getters['harvester/all'](HCI.UPGRADE).find( (U) => U.isLatestUpgrade);
|
|
||||||
},
|
|
||||||
resumeUpgradePausedNodeEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('resumeUpgradePausedNode');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
handleSwitch() {
|
handleSwitch() {
|
||||||
this.open = !this.open;
|
this.open = !this.open;
|
||||||
},
|
|
||||||
async resumeNodeUpgrade(nodeName) {
|
|
||||||
if (!this.latestUpgradeCR || !nodeName) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const upgradePauseMapString = this.latestUpgradeCR.metadata.annotations[HCI_ANNOTATIONS.NODE_UPGRADE_PAUSE_MAP] || '{}';
|
|
||||||
const upgradePauseMap = JSON.parse(upgradePauseMapString);
|
|
||||||
|
|
||||||
// update the upgrade CR annotation harvesterhci.io/node-upgrade-pause-map to unpause the node upgrade process
|
|
||||||
upgradePauseMap[`${ nodeName }`] = 'unpause';
|
|
||||||
this.latestUpgradeCR.setAnnotation(HCI_ANNOTATIONS.NODE_UPGRADE_PAUSE_MAP, JSON.stringify(upgradePauseMap));
|
|
||||||
await this.latestUpgradeCR.save();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`unable to update harvester upgrade CR annotations: ${ this.latestUpgradeCR.id }.`, e); // eslint-disable-line no-console
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -97,28 +63,12 @@ export default {
|
|||||||
v-for="(item, i) in list"
|
v-for="(item, i) in list"
|
||||||
:key="i"
|
:key="i"
|
||||||
>
|
>
|
||||||
<div class="upgrade-node-header">
|
<p>
|
||||||
<div class="upgrade-node-title">
|
{{ item.name }} <span
|
||||||
<p>
|
class="status"
|
||||||
{{ item.name }}
|
:class="{ [item.state]: true }"
|
||||||
</p>
|
>{{ item.state }}</span>
|
||||||
<span
|
</p>
|
||||||
class="status"
|
|
||||||
:class="{ [item.state]: true }"
|
|
||||||
>
|
|
||||||
{{ item.state }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-if="showResumeButton && resumeUpgradePausedNodeEnabled && item.state === 'Node-upgrade paused'"
|
|
||||||
type="button"
|
|
||||||
class="btn bg-info btn-sm"
|
|
||||||
data-testid="add-item"
|
|
||||||
@click="resumeNodeUpgrade(item.name)"
|
|
||||||
>
|
|
||||||
{{ t('action.resume') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<PercentageBar
|
<PercentageBar
|
||||||
:model-value="item.percent"
|
:model-value="item.percent"
|
||||||
preferred-direction="MORE"
|
preferred-direction="MORE"
|
||||||
@@ -152,21 +102,10 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.custom-content {
|
.custom-content {
|
||||||
.upgrade-node-title {
|
margin-bottom: 14px;
|
||||||
flex: 1 0 80%;
|
p {
|
||||||
margin-right: 10px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.upgrade-node-header {
|
|
||||||
display:flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
margin-bottom: 14px;
|
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
@@ -178,8 +117,6 @@ export default {
|
|||||||
}
|
}
|
||||||
.warning {
|
.warning {
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
margin-bottom: 8px;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const CATEGORY = {
|
|||||||
ui: [
|
ui: [
|
||||||
'branding',
|
'branding',
|
||||||
'ui-source',
|
'ui-source',
|
||||||
|
'ui-plugin-index',
|
||||||
'ui-index',
|
'ui-index',
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -257,12 +257,8 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
reconnect() {
|
|
||||||
this.$refs.novncConsole.reconnect();
|
|
||||||
},
|
|
||||||
|
|
||||||
softReboot() {
|
softReboot() {
|
||||||
this.vmResource.doSoftReboot();
|
this.vmResource.softrebootVM();
|
||||||
},
|
},
|
||||||
|
|
||||||
showKeysModal() {
|
showKeysModal() {
|
||||||
@@ -310,13 +306,6 @@ 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"
|
||||||
@@ -383,15 +372,3 @@ export default {
|
|||||||
background: rgb(40, 40, 40);
|
background: rgb(40, 40, 40);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.vm-console .v-popper__arrow-container {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.vm-console .v-popper__popper{
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
.vm-console .v-popper__inner{
|
|
||||||
overflow-y: visible;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
<script>
|
|
||||||
import MessageLink from '@shell/components/MessageLink';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import { HCI_SETTING } from '../../config/settings';
|
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterEditClusterRegistrationURL',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
LabeledInput, MessageLink, Checkbox, Banner
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
|
||||||
|
|
||||||
data() {
|
|
||||||
let parseDefaultValue = {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
parseDefaultValue = JSON.parse(this.value.value);
|
|
||||||
} catch (error) {
|
|
||||||
parseDefaultValue.url = this.value.value;
|
|
||||||
parseDefaultValue.insecureSkipTLSVerify = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
parseDefaultValue,
|
|
||||||
errors: []
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
toCA() {
|
|
||||||
return `${ HCI_SETTING.ADDITIONAL_CA }?mode=edit`;
|
|
||||||
},
|
|
||||||
clusterRegistrationTLSVerifyEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('clusterRegistrationTLSVerify');
|
|
||||||
},
|
|
||||||
registrationURL: {
|
|
||||||
get() {
|
|
||||||
return this.clusterRegistrationTLSVerifyEnabled ? this.parseDefaultValue.url : this.parseDefaultValue;
|
|
||||||
},
|
|
||||||
|
|
||||||
set(value) {
|
|
||||||
if (this.clusterRegistrationTLSVerifyEnabled) {
|
|
||||||
this.parseDefaultValue.url = value;
|
|
||||||
} else {
|
|
||||||
this.parseDefaultValue = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
getDefaultValue() {
|
|
||||||
if (this.clusterRegistrationTLSVerifyEnabled) {
|
|
||||||
return { url: '', insecureSkipTLSVerify: false };
|
|
||||||
} else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateUrl() {
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
update() {
|
|
||||||
if (this.clusterRegistrationTLSVerifyEnabled) {
|
|
||||||
this.value['value'] = JSON.stringify(this.parseDefaultValue);
|
|
||||||
} else {
|
|
||||||
this.value['value'] = this.parseDefaultValue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
useDefault() {
|
|
||||||
this.parseDefaultValue = this.getDefaultValue();
|
|
||||||
},
|
|
||||||
|
|
||||||
updateInsecureSkipTLSVerify(newValue) {
|
|
||||||
const { url = '' } = this.parseDefaultValue;
|
|
||||||
|
|
||||||
this.parseDefaultValue = { url, insecureSkipTLSVerify: newValue };
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="row"
|
|
||||||
>
|
|
||||||
<div class="col span-12">
|
|
||||||
<Banner color="info">
|
|
||||||
<MessageLink
|
|
||||||
:to="toCA"
|
|
||||||
target="_blank"
|
|
||||||
prefix-label="harvester.setting.clusterRegistrationUrl.tip.prefix"
|
|
||||||
middle-label="harvester.setting.clusterRegistrationUrl.tip.middle"
|
|
||||||
suffix-label="harvester.setting.clusterRegistrationUrl.tip.suffix"
|
|
||||||
/>
|
|
||||||
</Banner>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="registrationURL"
|
|
||||||
class="mb-20"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.setting.clusterRegistrationUrl.url')"
|
|
||||||
@update:value="updateUrl"
|
|
||||||
/>
|
|
||||||
<div v-if="clusterRegistrationTLSVerifyEnabled">
|
|
||||||
<Checkbox
|
|
||||||
v-model:value="parseDefaultValue.insecureSkipTLSVerify"
|
|
||||||
class="check mb-5"
|
|
||||||
type="checkbox"
|
|
||||||
:label="t('harvester.setting.clusterRegistrationUrl.insecureSkipTLSVerify')"
|
|
||||||
@update:value="updateInsecureSkipTLSVerify"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
<script>
|
|
||||||
import UnitInput from '@shell/components/form/UnitInput';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterInstanceManagerResources',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
UnitInput,
|
|
||||||
Banner,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({
|
|
||||||
value: '',
|
|
||||||
default: '{}'
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
const resources = this.parseJSON(this.value?.value) || this.parseJSON(this.value?.default) || {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
resources,
|
|
||||||
parseError: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
parseJSON(string) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(string);
|
|
||||||
} catch (e) {
|
|
||||||
this.parseError = this.t('harvester.setting.instanceManagerResources.parseError', { error: e.message });
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
update() {
|
|
||||||
if (!this.value) return;
|
|
||||||
|
|
||||||
const cpu = { ...this.resources?.cpu };
|
|
||||||
|
|
||||||
if (cpu.v1 !== null && cpu.v1 !== undefined) cpu.v1 = String(cpu.v1);
|
|
||||||
if (cpu.v2 !== null && cpu.v2 !== undefined) cpu.v2 = String(cpu.v2);
|
|
||||||
|
|
||||||
this.value.value = JSON.stringify({ ...this.resources, cpu });
|
|
||||||
},
|
|
||||||
|
|
||||||
useDefault() {
|
|
||||||
if (this.value?.default) {
|
|
||||||
this.resources = this.parseJSON(this.value.default) || {};
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<Banner
|
|
||||||
v-if="parseError"
|
|
||||||
color="error"
|
|
||||||
>
|
|
||||||
{{ parseError }}
|
|
||||||
</Banner>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="resources.cpu.v1"
|
|
||||||
:label="t('harvester.setting.instanceManagerResources.v1')"
|
|
||||||
suffix="%"
|
|
||||||
:delay="0"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
required
|
|
||||||
:mode="mode"
|
|
||||||
class="mb-20"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="resources.cpu.v2"
|
|
||||||
:label="t('harvester.setting.instanceManagerResources.v2')"
|
|
||||||
suffix="%"
|
|
||||||
:delay="0"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
required
|
|
||||||
:mode="mode"
|
|
||||||
class="mb-20"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
|
||||||
import UnitInput from '@shell/components/form/UnitInput';
|
|
||||||
import { MEBIBYTE } from '../../utils/unit';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'KubevirtMigration',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
LabeledInput,
|
|
||||||
UnitInput,
|
|
||||||
RadioGroup,
|
|
||||||
Banner
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
registerBeforeHook: {
|
|
||||||
type: Function,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({
|
|
||||||
value: '',
|
|
||||||
default: '{}'
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
const migration = this.parseJSON(this.value?.value) || this.parseJSON(this.value?.default) || {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
MEBIBYTE,
|
|
||||||
migration,
|
|
||||||
parseError: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.registerBeforeHook?.(this.willSave, 'willSave');
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
parseJSON(string) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(string);
|
|
||||||
} catch (e) {
|
|
||||||
this.parseError = this.t('kubevirtMigration.parseError', { error: e.message });
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error('Failed to parse JSON:', e.message);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateValue() {
|
|
||||||
if (this.value) {
|
|
||||||
this.value.value = JSON.stringify(this.migration);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
useDefault() {
|
|
||||||
if (this.value?.default) {
|
|
||||||
const defaultMigration = this.parseJSON(this.value.default) || {};
|
|
||||||
|
|
||||||
this.migration = defaultMigration;
|
|
||||||
this.updateValue();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async willSave() {
|
|
||||||
this.updateValue();
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<Banner
|
|
||||||
v-if="parseError"
|
|
||||||
color="error"
|
|
||||||
>
|
|
||||||
{{ parseError }}
|
|
||||||
</Banner>
|
|
||||||
|
|
||||||
<div class="migration-field">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value.number="migration.parallelMigrationsPerCluster"
|
|
||||||
:label="t('harvester.setting.kubevirtMigration.parallelMigrationsPerCluster')"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
/>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value.number="migration.parallelOutboundMigrationsPerNode"
|
|
||||||
:label="t('harvester.setting.kubevirtMigration.parallelOutboundMigrationsPerNode')"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
/>
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="migration.bandwidthPerMigration"
|
|
||||||
min="0"
|
|
||||||
:label="t('harvester.setting.kubevirtMigration.bandwidthPerMigration')"
|
|
||||||
:mode="mode"
|
|
||||||
:suffix="MEBIBYTE"
|
|
||||||
:tooltip="t('harvester.setting.kubevirtMigration.bandwidthPerMigrationTooltip', _, true)"
|
|
||||||
/>
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="migration.completionTimeoutPerGiB"
|
|
||||||
:label="t('harvester.setting.kubevirtMigration.completionTimeoutPerGiB')"
|
|
||||||
:mode="mode"
|
|
||||||
:suffix="migration.completionTimeoutPerGiB === 1 ? 'Second' : 'Seconds'"
|
|
||||||
min="10"
|
|
||||||
/>
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="migration.progressTimeout"
|
|
||||||
:label="t('harvester.setting.kubevirtMigration.progressTimeout')"
|
|
||||||
:mode="mode"
|
|
||||||
:suffix="migration.progressTimeout === 1 ? 'Second' : 'Seconds'"
|
|
||||||
min="10"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-for="field in ['allowAutoConverge','allowPostCopy','unsafeMigrationOverride','allowWorkloadDisruption','disableTLS','matchSELinuxLevelOnMigration']"
|
|
||||||
:key="field"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="mb-5"
|
|
||||||
:for="field"
|
|
||||||
>{{ t(`harvester.setting.kubevirtMigration.${field}`) }}</label>
|
|
||||||
<RadioGroup
|
|
||||||
:id="field"
|
|
||||||
v-model:value="migration[field]"
|
|
||||||
:options="[
|
|
||||||
{ label: t('advancedSettings.edit.trueOption'), value: true },
|
|
||||||
{ label: t('advancedSettings.edit.falseOption'), value: false },
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.migration-field {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,371 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
|
||||||
import ArrayList from '@shell/components/form/ArrayList';
|
|
||||||
import { isValidCIDR } from '@shell/utils/validators/cidr';
|
|
||||||
import { _EDIT } from '@shell/config/query-params';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
|
||||||
import { HCI } from '../../types';
|
|
||||||
import { NETWORK_TYPE } from '../../config/types';
|
|
||||||
|
|
||||||
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
|
|
||||||
const SHARE_STORAGE_NETWORK = 'share-storage-network';
|
|
||||||
const NETWORK = 'network';
|
|
||||||
|
|
||||||
const DEFAULT_DEDICATED_NETWORK = {
|
|
||||||
vlan: '',
|
|
||||||
clusterNetwork: '',
|
|
||||||
range: '',
|
|
||||||
exclude: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'RwxNetworkSetting',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
RadioGroup,
|
|
||||||
Banner,
|
|
||||||
ArrayList,
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
registerBeforeHook: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
default: _EDIT,
|
|
||||||
},
|
|
||||||
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
await allHash({
|
|
||||||
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
|
|
||||||
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
let enabled = false; // enabled / disabled options
|
|
||||||
let shareStorageNetwork = false; // shareStorageNetwork / dedicatedRwxNetwork options
|
|
||||||
let dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
|
||||||
let networkType = L2VLAN;
|
|
||||||
let exclude = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsedValue = JSON.parse(this.value.value || this.value.default || '{}');
|
|
||||||
const parsedNetwork = parsedValue?.[NETWORK] || parsedValue || {};
|
|
||||||
|
|
||||||
if (parsedValue && typeof parsedValue === 'object') {
|
|
||||||
shareStorageNetwork = !!parsedValue[SHARE_STORAGE_NETWORK];
|
|
||||||
networkType = 'vlan' in parsedNetwork ? L2VLAN : UNTAGGED;
|
|
||||||
dedicatedNetwork = {
|
|
||||||
vlan: parsedNetwork.vlan || '',
|
|
||||||
clusterNetwork: parsedNetwork.clusterNetwork || '',
|
|
||||||
range: parsedNetwork.range || '',
|
|
||||||
};
|
|
||||||
exclude = parsedNetwork?.exclude?.toString().split(',') || [];
|
|
||||||
enabled = shareStorageNetwork || !!(parsedNetwork.vlan || parsedNetwork.clusterNetwork || parsedNetwork.range);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
enabled = false;
|
|
||||||
shareStorageNetwork = false;
|
|
||||||
dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabled,
|
|
||||||
shareStorageNetwork,
|
|
||||||
dedicatedNetwork,
|
|
||||||
networkType,
|
|
||||||
exclude,
|
|
||||||
defaultAddValue: '',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
if (this.registerBeforeHook) {
|
|
||||||
this.registerBeforeHook(this.willSave, 'willSave');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
showDedicatedNetworkConfig() {
|
|
||||||
return this.enabled && !this.shareStorageNetwork;
|
|
||||||
},
|
|
||||||
|
|
||||||
showVlan() {
|
|
||||||
return this.networkType === L2VLAN;
|
|
||||||
},
|
|
||||||
|
|
||||||
networkTypes() {
|
|
||||||
return [L2VLAN, UNTAGGED];
|
|
||||||
},
|
|
||||||
|
|
||||||
clusterNetworkOptions() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
|
||||||
const clusterNetworksOptions = this.networkType === UNTAGGED ? clusterNetworks.filter((network) => network.id !== 'mgmt') : clusterNetworks;
|
|
||||||
|
|
||||||
return clusterNetworksOptions.map((network) => {
|
|
||||||
const disabled = !network.isReadyForStorageNetwork;
|
|
||||||
|
|
||||||
return {
|
|
||||||
label: disabled ? `${ network.id } (${ this.t('generic.notReady') })` : network.id,
|
|
||||||
value: network.id,
|
|
||||||
disabled,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
onUpdateEnabled() {
|
|
||||||
if (!this.enabled) {
|
|
||||||
this.shareStorageNetwork = false;
|
|
||||||
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
|
||||||
}
|
|
||||||
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
onUpdateNetworkType() {
|
|
||||||
if (this.shareStorageNetwork) {
|
|
||||||
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
|
||||||
}
|
|
||||||
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
onUpdateDedicatedType(neu) {
|
|
||||||
this.dedicatedNetwork.clusterNetwork = '';
|
|
||||||
|
|
||||||
if (neu === L2VLAN) {
|
|
||||||
this.dedicatedNetwork.vlan = '';
|
|
||||||
} else {
|
|
||||||
delete this.dedicatedNetwork.vlan;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
inputVlan(neu) {
|
|
||||||
if (neu === '') {
|
|
||||||
this.dedicatedNetwork.vlan = '';
|
|
||||||
this.update();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newValue = Number(neu);
|
|
||||||
|
|
||||||
if (newValue > 4094) {
|
|
||||||
this.dedicatedNetwork.vlan = 4094;
|
|
||||||
} else if (newValue < 1) {
|
|
||||||
this.dedicatedNetwork.vlan = 1;
|
|
||||||
} else {
|
|
||||||
this.dedicatedNetwork.vlan = newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
useDefault() {
|
|
||||||
this.enabled = false;
|
|
||||||
this.shareStorageNetwork = false;
|
|
||||||
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
update() {
|
|
||||||
const value = { [SHARE_STORAGE_NETWORK]: false };
|
|
||||||
|
|
||||||
if (this.enabled && this.shareStorageNetwork) {
|
|
||||||
value[SHARE_STORAGE_NETWORK] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.showDedicatedNetworkConfig) {
|
|
||||||
value[NETWORK] = {};
|
|
||||||
|
|
||||||
if (this.showVlan) {
|
|
||||||
value[NETWORK].vlan = this.dedicatedNetwork.vlan;
|
|
||||||
}
|
|
||||||
|
|
||||||
value[NETWORK].clusterNetwork = this.dedicatedNetwork.clusterNetwork;
|
|
||||||
value[NETWORK].range = this.dedicatedNetwork.range;
|
|
||||||
|
|
||||||
const excludeList = this.exclude.filter((ip) => ip);
|
|
||||||
|
|
||||||
if (Array.isArray(excludeList) && excludeList.length > 0) {
|
|
||||||
value[NETWORK].exclude = excludeList;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value.value = JSON.stringify(value);
|
|
||||||
},
|
|
||||||
|
|
||||||
willSave() {
|
|
||||||
this.update();
|
|
||||||
|
|
||||||
if (!this.showDedicatedNetworkConfig) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
if (this.showVlan && !this.dedicatedNetwork.vlan) {
|
|
||||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.dedicatedNetwork.clusterNetwork) {
|
|
||||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.clusterNetwork') }, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.dedicatedNetwork.range) {
|
|
||||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.range.label') }, true));
|
|
||||||
} else if (!isValidCIDR(this.dedicatedNetwork.range)) {
|
|
||||||
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.exclude) {
|
|
||||||
const hasInvalidCIDR = this.exclude.find((cidr) => {
|
|
||||||
return cidr && !isValidCIDR(cidr);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (hasInvalidCIDR) {
|
|
||||||
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', null, true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
return Promise.reject(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div :class="mode">
|
|
||||||
<Banner color="warning">
|
|
||||||
<t
|
|
||||||
k="harvester.setting.rwxNetwork.warning"
|
|
||||||
:raw="true"
|
|
||||||
/>
|
|
||||||
</Banner>
|
|
||||||
<RadioGroup
|
|
||||||
v-model:value="enabled"
|
|
||||||
class="mb-20"
|
|
||||||
name="rwx-network-enable"
|
|
||||||
:options="[true,false]"
|
|
||||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
|
||||||
@update:value="onUpdateEnabled"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RadioGroup
|
|
||||||
v-if="enabled"
|
|
||||||
v-model:value="shareStorageNetwork"
|
|
||||||
class="mb-20"
|
|
||||||
name="rwx-network-type"
|
|
||||||
:options="[true,false]"
|
|
||||||
:labels="[t('harvester.setting.rwxNetwork.shareStorageNetwork'), t('harvester.setting.rwxNetwork.dedicatedRwxNetwork')]"
|
|
||||||
@update:value="onUpdateNetworkType"
|
|
||||||
/>
|
|
||||||
<Banner
|
|
||||||
v-if="shareStorageNetwork"
|
|
||||||
class="mb-20"
|
|
||||||
color="warning"
|
|
||||||
>
|
|
||||||
<t
|
|
||||||
k="harvester.setting.rwxNetwork.shareStorageNetworkWarning"
|
|
||||||
:raw="true"
|
|
||||||
/>
|
|
||||||
</Banner>
|
|
||||||
<template v-if="showDedicatedNetworkConfig">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="networkType"
|
|
||||||
class="mb-20"
|
|
||||||
:options="networkTypes"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.fields.type')"
|
|
||||||
required
|
|
||||||
@update:value="onUpdateDedicatedType"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledInput
|
|
||||||
v-if="showVlan"
|
|
||||||
v-model:value.number="dedicatedNetwork.vlan"
|
|
||||||
type="number"
|
|
||||||
class="mb-20"
|
|
||||||
:mode="mode"
|
|
||||||
required
|
|
||||||
placeholder="e.g. 1 - 4094"
|
|
||||||
label-key="harvester.setting.storageNetwork.vlan"
|
|
||||||
@update:value="inputVlan"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="dedicatedNetwork.clusterNetwork"
|
|
||||||
label-key="harvester.setting.storageNetwork.clusterNetwork"
|
|
||||||
class="mb-20"
|
|
||||||
required
|
|
||||||
:options="clusterNetworkOptions"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="dedicatedNetwork.range"
|
|
||||||
class="mb-5"
|
|
||||||
:mode="mode"
|
|
||||||
required
|
|
||||||
:placeholder="t('harvester.setting.storageNetwork.range.placeholder')"
|
|
||||||
label-key="harvester.setting.storageNetwork.range.label"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ArrayList
|
|
||||||
v-model:value="exclude"
|
|
||||||
:show-header="true"
|
|
||||||
:default-add-value="defaultAddValue"
|
|
||||||
:mode="mode"
|
|
||||||
:add-label="t('harvester.setting.storageNetwork.exclude.addIp')"
|
|
||||||
class="mt-20"
|
|
||||||
@update:value="update"
|
|
||||||
>
|
|
||||||
<template #column-headers>
|
|
||||||
<div class="box mb-10">
|
|
||||||
<div class="key">
|
|
||||||
{{ t('harvester.setting.storageNetwork.exclude.label') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #columns="scope">
|
|
||||||
<div class="key">
|
|
||||||
<input
|
|
||||||
v-model="scope.row.value"
|
|
||||||
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ArrayList>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -18,20 +18,16 @@ export default {
|
|||||||
|
|
||||||
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
||||||
|
|
||||||
if (this.customSupportBundleFeatureEnabled) {
|
try {
|
||||||
try {
|
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
||||||
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
const response = await this.$store.dispatch('harvester/request', { url });
|
||||||
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() {
|
||||||
@@ -46,38 +42,24 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
customSupportBundleFeatureEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
|
|
||||||
},
|
|
||||||
|
|
||||||
allNamespaces() {
|
allNamespaces() {
|
||||||
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
||||||
},
|
},
|
||||||
|
|
||||||
filteredNamespaces() {
|
filteredNamespaces() {
|
||||||
if (!this.customSupportBundleFeatureEnabled) {
|
|
||||||
return this.allNamespaces;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
|
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
|
||||||
|
|
||||||
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
|
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
|
||||||
},
|
},
|
||||||
|
|
||||||
namespaceOptions() {
|
namespaceOptions() {
|
||||||
const mappedNamespaces = this.filteredNamespaces.map((ns) => ({ label: ns, value: ns }));
|
|
||||||
|
|
||||||
if (!this.customSupportBundleFeatureEnabled) {
|
|
||||||
return mappedNamespaces;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allSelected =
|
const allSelected =
|
||||||
this.namespaces.length === this.filteredNamespaces.length &&
|
this.namespaces.length === this.filteredNamespaces.length &&
|
||||||
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
|
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
|
||||||
|
|
||||||
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
|
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
|
||||||
|
|
||||||
return [controlOption, ...mappedNamespaces];
|
return [controlOption, ...this.filteredNamespaces];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
|||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { allHash } from '@shell/utils/promise';
|
|
||||||
import { NODE } from '@shell/config/types';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterUpgradeConfig',
|
name: 'HarvesterUpgradeConfig',
|
||||||
@@ -17,13 +15,6 @@ export default {
|
|||||||
},
|
},
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const hash = { nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }) };
|
|
||||||
|
|
||||||
await allHash(hash);
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
let parseDefaultValue = {};
|
let parseDefaultValue = {};
|
||||||
|
|
||||||
@@ -48,25 +39,7 @@ export default {
|
|||||||
{ value: 'skip', label: 'skip' },
|
{ value: 'skip', label: 'skip' },
|
||||||
{ value: 'parallel', label: 'parallel' }
|
{ value: 'parallel', label: 'parallel' }
|
||||||
];
|
];
|
||||||
},
|
}
|
||||||
nodeUpgradeOptions() {
|
|
||||||
return [
|
|
||||||
{ value: 'auto', label: 'auto' },
|
|
||||||
{ value: 'manual', label: 'manual' }
|
|
||||||
];
|
|
||||||
},
|
|
||||||
nodesOptions() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const nodes = this.$store.getters[`${ inStore }/all`](NODE);
|
|
||||||
|
|
||||||
return nodes.map((node) => ({ value: node.id, label: node.name }));
|
|
||||||
},
|
|
||||||
showPauseNodes() {
|
|
||||||
return this.parseDefaultValue.nodeUpgradeOption?.strategy?.mode === 'manual';
|
|
||||||
},
|
|
||||||
resumeUpgradePausedNodeEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('resumeUpgradePausedNode');
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
@@ -75,18 +48,6 @@ export default {
|
|||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
normalizeValue(obj) {
|
normalizeValue(obj) {
|
||||||
// handle nodeUpgradeOption.strategy
|
|
||||||
if (obj?.nodeUpgradeOption?.strategy?.mode === 'auto') {
|
|
||||||
delete obj.nodeUpgradeOption.strategy.pauseNodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (obj?.nodeUpgradeOption?.strategy?.mode === 'manual') {
|
|
||||||
if (!Array.isArray(obj.nodeUpgradeOption.strategy.pauseNodes)) {
|
|
||||||
obj.nodeUpgradeOption.strategy.pauseNodes = this.nodesOptions.map((node) => node.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle imagePreloadOption.strategy
|
|
||||||
if (!obj.imagePreloadOption) {
|
if (!obj.imagePreloadOption) {
|
||||||
obj.imagePreloadOption = { strategy: { type: 'sequential' } };
|
obj.imagePreloadOption = { strategy: { type: 'sequential' } };
|
||||||
}
|
}
|
||||||
@@ -144,8 +105,8 @@ export default {
|
|||||||
this.update();
|
this.update();
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -183,28 +144,6 @@ export default {
|
|||||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
<div v-if="resumeUpgradePausedNodeEnabled">
|
|
||||||
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.nodeUpgradeOption') }}</b></label>
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="parseDefaultValue.nodeUpgradeOption.strategy.mode"
|
|
||||||
class="mb-20 label-select"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.setting.upgrade.strategy')"
|
|
||||||
:options="nodeUpgradeOptions"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
<LabeledSelect
|
|
||||||
v-if="showPauseNodes"
|
|
||||||
v-model:value="parseDefaultValue.nodeUpgradeOption.strategy.pauseNodes"
|
|
||||||
class="mb-20 label-select"
|
|
||||||
:clearable="true"
|
|
||||||
:multiple="true"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.setting.upgrade.pauseNodes')"
|
|
||||||
:options="nodesOptions"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-if="errors.length"
|
v-if="errors.length"
|
||||||
class="error"
|
class="error"
|
||||||
|
|||||||
@@ -69,15 +69,6 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
label-key="harvester.setting.vmForceDeletionPolicy.period"
|
label-key="harvester.setting.vmForceDeletionPolicy.period"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledInput
|
|
||||||
v-if="parseDefaultValue.enable"
|
|
||||||
v-model:value.number="parseDefaultValue.vmMigrationTimeout"
|
|
||||||
class="mb-20"
|
|
||||||
type="number"
|
|
||||||
:mode="mode"
|
|
||||||
label-key="harvester.setting.vmForceDeletionPolicy.vmMigrationTimeout"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
// 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,6 +39,7 @@ const FEATURE_FLAGS = {
|
|||||||
],
|
],
|
||||||
'v1.5.1': [],
|
'v1.5.1': [],
|
||||||
'v1.6.0': [
|
'v1.6.0': [
|
||||||
|
'vmMachineTypes',
|
||||||
'customSupportBundle',
|
'customSupportBundle',
|
||||||
'csiOnlineExpandValidation',
|
'csiOnlineExpandValidation',
|
||||||
'vmNetworkMigration',
|
'vmNetworkMigration',
|
||||||
@@ -47,26 +48,7 @@ const FEATURE_FLAGS = {
|
|||||||
'cpuMemoryHotplug',
|
'cpuMemoryHotplug',
|
||||||
'cdiSettings',
|
'cdiSettings',
|
||||||
'vmCloneRunStrategy',
|
'vmCloneRunStrategy',
|
||||||
],
|
]
|
||||||
'v1.6.1': [],
|
|
||||||
'v1.7.0': [
|
|
||||||
'vmMachineTypeAuto',
|
|
||||||
'lhV2VolExpansion',
|
|
||||||
'l2VlanTrunkMode',
|
|
||||||
'kubevirtMigration',
|
|
||||||
'hotplugNic',
|
|
||||||
'resumeUpgradePausedNode',
|
|
||||||
],
|
|
||||||
'v1.7.1': [],
|
|
||||||
'v1.8.0': [
|
|
||||||
'hotplugCdRom',
|
|
||||||
'supportBundleFileNameSetting',
|
|
||||||
'clusterRegistrationTLSVerify',
|
|
||||||
'vGPUAsPCIDevice',
|
|
||||||
'instanceManagerResourcesSetting',
|
|
||||||
'rwxNetworkSetting',
|
|
||||||
'createPVCWithDataVolume'
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateFeatureFlags = () => {
|
const generateFeatureFlags = () => {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ 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 {
|
||||||
@@ -35,21 +34,8 @@ import {
|
|||||||
SNAPSHOT_TARGET_VOLUME,
|
SNAPSHOT_TARGET_VOLUME,
|
||||||
IMAGE_VIRTUAL_SIZE,
|
IMAGE_VIRTUAL_SIZE,
|
||||||
IMAGE_STORAGE_CLASS,
|
IMAGE_STORAGE_CLASS,
|
||||||
HARVESTER_DESCRIPTION,
|
HARVESTER_DESCRIPTION
|
||||||
VM_IMPORT_SOURCE_VM,
|
|
||||||
VM_IMPORT_SOURCE_CLUSTER,
|
|
||||||
VM_IMPORT_STATUS,
|
|
||||||
VM_IMPORT_SOURCE_V_DC,
|
|
||||||
VM_IMPORT_SOURCE_V_ENDPOINT,
|
|
||||||
VM_IMPORT_SOURCE_V_STATUS,
|
|
||||||
VM_IMPORT_SOURCE_O_REGION,
|
|
||||||
VM_IMPORT_SOURCE_O_ENDPOINT,
|
|
||||||
VM_IMPORT_SOURCE_O_STATUS,
|
|
||||||
VM_IMPORT_SOURCE_OVA_URL,
|
|
||||||
VM_IMPORT_SOURCE_OVA_STATUS,
|
|
||||||
} from './table-headers';
|
} from './table-headers';
|
||||||
import { ADD_ONS } from './harvester-map';
|
|
||||||
import { registerAddonSideNav } from '../utils/dynamic-nav';
|
|
||||||
|
|
||||||
const TEMPLATE = HCI.VM_VERSION;
|
const TEMPLATE = HCI.VM_VERSION;
|
||||||
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
|
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
|
||||||
@@ -208,142 +194,6 @@ export function init($plugin, store) {
|
|||||||
exact: false
|
exact: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===========================================================================
|
|
||||||
// VM Import Controller UI Flow
|
|
||||||
// ===========================================================================
|
|
||||||
// Define group (Hidden by default)
|
|
||||||
weightGroup('vmimport', 0, false);
|
|
||||||
|
|
||||||
// VirtualMachineImport
|
|
||||||
headers(HCI.VMIMPORT, [
|
|
||||||
STATE,
|
|
||||||
NAME_COL,
|
|
||||||
NAMESPACE_COL,
|
|
||||||
VM_IMPORT_SOURCE_VM,
|
|
||||||
VM_IMPORT_SOURCE_CLUSTER,
|
|
||||||
VM_IMPORT_STATUS,
|
|
||||||
AGE
|
|
||||||
]);
|
|
||||||
configureType(HCI.VMIMPORT, {
|
|
||||||
resource: HCI.VMIMPORT,
|
|
||||||
resourceDetail: HCI.VMIMPORT,
|
|
||||||
resourceEdit: HCI.VMIMPORT,
|
|
||||||
location: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.VMIMPORT }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
|
|
||||||
name: HCI.VMIMPORT,
|
|
||||||
labelKey: 'harvester.addons.vmImport.labels.vmimport',
|
|
||||||
group: 'vmimport',
|
|
||||||
namespaced: true,
|
|
||||||
route: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.VMIMPORT }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Source: VMware
|
|
||||||
headers(HCI.VMIMPORT_SOURCE_V, [
|
|
||||||
STATE,
|
|
||||||
NAME_COL,
|
|
||||||
VM_IMPORT_SOURCE_V_ENDPOINT,
|
|
||||||
VM_IMPORT_SOURCE_V_DC,
|
|
||||||
VM_IMPORT_SOURCE_V_STATUS,
|
|
||||||
AGE
|
|
||||||
]);
|
|
||||||
configureType(HCI.VMIMPORT_SOURCE_V, {
|
|
||||||
resource: HCI.VMIMPORT_SOURCE_V,
|
|
||||||
resourceDetail: HCI.VMIMPORT_SOURCE_V,
|
|
||||||
resourceEdit: HCI.VMIMPORT_SOURCE_V,
|
|
||||||
location: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.VMIMPORT_SOURCE_V }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
|
|
||||||
name: HCI.VMIMPORT_SOURCE_V,
|
|
||||||
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare',
|
|
||||||
group: 'vmimport',
|
|
||||||
namespaced: true,
|
|
||||||
route: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.VMIMPORT_SOURCE_V }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Source: OpenStack
|
|
||||||
headers(HCI.VMIMPORT_SOURCE_O, [
|
|
||||||
STATE,
|
|
||||||
NAME_COL,
|
|
||||||
VM_IMPORT_SOURCE_O_ENDPOINT,
|
|
||||||
VM_IMPORT_SOURCE_O_REGION,
|
|
||||||
VM_IMPORT_SOURCE_O_STATUS,
|
|
||||||
AGE
|
|
||||||
]);
|
|
||||||
configureType(HCI.VMIMPORT_SOURCE_O, {
|
|
||||||
resource: HCI.VMIMPORT_SOURCE_O,
|
|
||||||
resourceDetail: HCI.VMIMPORT_SOURCE_O,
|
|
||||||
resourceEdit: HCI.VMIMPORT_SOURCE_O,
|
|
||||||
location: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.VMIMPORT_SOURCE_O }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
|
|
||||||
name: HCI.VMIMPORT_SOURCE_O,
|
|
||||||
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack',
|
|
||||||
group: 'vmimport',
|
|
||||||
namespaced: true,
|
|
||||||
route: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.VMIMPORT_SOURCE_O }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Source: OVA
|
|
||||||
headers(HCI.VMIMPORT_SOURCE_OVA, [
|
|
||||||
STATE,
|
|
||||||
NAME_COL,
|
|
||||||
VM_IMPORT_SOURCE_OVA_URL,
|
|
||||||
VM_IMPORT_SOURCE_OVA_STATUS,
|
|
||||||
AGE
|
|
||||||
]);
|
|
||||||
configureType(HCI.VMIMPORT_SOURCE_OVA, {
|
|
||||||
resource: HCI.VMIMPORT_SOURCE_OVA,
|
|
||||||
resourceDetail: HCI.VMIMPORT_SOURCE_OVA,
|
|
||||||
resourceEdit: HCI.VMIMPORT_SOURCE_OVA,
|
|
||||||
location: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.VMIMPORT_SOURCE_OVA }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
|
|
||||||
name: HCI.VMIMPORT_SOURCE_OVA,
|
|
||||||
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA',
|
|
||||||
group: 'vmimport',
|
|
||||||
namespaced: true,
|
|
||||||
route: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.VMIMPORT_SOURCE_OVA }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Enable SideNav based on Addon Status
|
|
||||||
registerAddonSideNav(store, PRODUCT_NAME, {
|
|
||||||
addonName: ADD_ONS.VM_IMPORT_CONTROLLER,
|
|
||||||
resourceType: HCI.ADD_ONS,
|
|
||||||
navGroup: 'vmimport',
|
|
||||||
types: [
|
|
||||||
HCI.VMIMPORT_SOURCE_V,
|
|
||||||
HCI.VMIMPORT_SOURCE_O,
|
|
||||||
HCI.VMIMPORT_SOURCE_OVA,
|
|
||||||
HCI.VMIMPORT
|
|
||||||
]
|
|
||||||
});
|
|
||||||
// ===========================================================================
|
|
||||||
|
|
||||||
basicType([HCI.VOLUME]);
|
basicType([HCI.VOLUME]);
|
||||||
configureType(HCI.VOLUME, {
|
configureType(HCI.VOLUME, {
|
||||||
location: {
|
location: {
|
||||||
@@ -575,7 +425,6 @@ 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,
|
||||||
],
|
],
|
||||||
@@ -606,7 +455,6 @@ 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,
|
||||||
@@ -718,21 +566,6 @@ 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: {
|
||||||
@@ -999,26 +832,6 @@ export function init($plugin, store) {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
virtualType({
|
|
||||||
labelKey: 'harvester.migconfiguration.label',
|
|
||||||
group: 'advanced',
|
|
||||||
weight: 12,
|
|
||||||
name: HCI.MIG_CONFIGURATION,
|
|
||||||
namespaced: false,
|
|
||||||
route: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.MIG_CONFIGURATION }
|
|
||||||
},
|
|
||||||
exact: false,
|
|
||||||
ifHaveType: HCI.MIG_CONFIGURATION,
|
|
||||||
});
|
|
||||||
|
|
||||||
configureType(HCI.MIG_CONFIGURATION, {
|
|
||||||
isCreatable: false,
|
|
||||||
hiddenNamespaceGroupButton: true,
|
|
||||||
canYaml: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
labelKey: 'harvester.usb.label',
|
labelKey: 'harvester.usb.label',
|
||||||
group: 'advanced',
|
group: 'advanced',
|
||||||
|
|||||||
@@ -39,12 +39,6 @@ export const VOLUME_TYPE = [{
|
|||||||
value: 'cd-rom'
|
value: 'cd-rom'
|
||||||
}];
|
}];
|
||||||
|
|
||||||
export const VOLUME_HOTPLUG_ACTION = {
|
|
||||||
INSERT_CDROM_IMAGE: 'INSERT_CDROM_IMAGE',
|
|
||||||
EJECT_CDROM_IMAGE: 'EJECT_CDROM_IMAGE',
|
|
||||||
DETACH_DISK: 'DETACH_DISK'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ACCESS_CREDENTIALS = {
|
export const ACCESS_CREDENTIALS = {
|
||||||
RESET_PWD: 'userPassword',
|
RESET_PWD: 'userPassword',
|
||||||
INJECT_SSH: 'sshPublicKey'
|
INJECT_SSH: 'sshPublicKey'
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export const HCI = {
|
|||||||
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
|
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
|
||||||
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
|
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
|
||||||
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
|
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
|
||||||
CLONE_BACKEND_STORAGE_STATUS: 'harvesterhci.io/clone-backend-storage-status',
|
|
||||||
MIGRATION_STATE: 'harvesterhci.io/migrationState',
|
MIGRATION_STATE: 'harvesterhci.io/migrationState',
|
||||||
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
|
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
|
||||||
IMAGE_NAME: 'harvesterhci.io/image-name',
|
IMAGE_NAME: 'harvesterhci.io/image-name',
|
||||||
@@ -51,7 +50,6 @@ 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',
|
||||||
@@ -78,7 +76,4 @@ export const HCI = {
|
|||||||
CLONE_STRATEGY: 'cdi.harvesterhci.io/storageProfileCloneStrategy',
|
CLONE_STRATEGY: 'cdi.harvesterhci.io/storageProfileCloneStrategy',
|
||||||
VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes',
|
VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes',
|
||||||
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
|
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
|
||||||
MAC_ADDRESS: 'harvesterhci.io/mac-address',
|
|
||||||
NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map',
|
|
||||||
CDI_POPULATOR_KIND: 'cdi.kubevirt.io/storage.populator.kind',
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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',
|
||||||
@@ -16,11 +17,9 @@ export const HCI_SETTING = {
|
|||||||
DEFAULT_STORAGE_CLASS: 'default-storage-class',
|
DEFAULT_STORAGE_CLASS: 'default-storage-class',
|
||||||
SUPPORT_BUNDLE_TIMEOUT: 'support-bundle-timeout',
|
SUPPORT_BUNDLE_TIMEOUT: 'support-bundle-timeout',
|
||||||
SUPPORT_BUNDLE_EXPIRATION: 'support-bundle-expiration',
|
SUPPORT_BUNDLE_EXPIRATION: 'support-bundle-expiration',
|
||||||
SUPPORT_BUNDLE_FILE_NAME: 'support-bundle-file-name',
|
|
||||||
SUPPORT_BUNDLE_IMAGE: 'support-bundle-image',
|
SUPPORT_BUNDLE_IMAGE: 'support-bundle-image',
|
||||||
SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT: 'support-bundle-node-collection-timeout',
|
SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT: 'support-bundle-node-collection-timeout',
|
||||||
STORAGE_NETWORK: 'storage-network',
|
STORAGE_NETWORK: 'storage-network',
|
||||||
RWX_NETWORK: 'rwx-network',
|
|
||||||
VM_FORCE_RESET_POLICY: 'vm-force-reset-policy',
|
VM_FORCE_RESET_POLICY: 'vm-force-reset-policy',
|
||||||
SSL_CERTIFICATES: 'ssl-certificates',
|
SSL_CERTIFICATES: 'ssl-certificates',
|
||||||
SSL_PARAMETERS: 'ssl-parameters',
|
SSL_PARAMETERS: 'ssl-parameters',
|
||||||
@@ -39,9 +38,7 @@ export const HCI_SETTING = {
|
|||||||
UPGRADE_CONFIG: 'upgrade-config',
|
UPGRADE_CONFIG: 'upgrade-config',
|
||||||
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
||||||
RANCHER_CLUSTER: 'rancher-cluster',
|
RANCHER_CLUSTER: 'rancher-cluster',
|
||||||
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
|
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio'
|
||||||
KUBEVIRT_MIGRATION: 'kubevirt-migration',
|
|
||||||
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_ALLOWED_SETTINGS = {
|
export const HCI_ALLOWED_SETTINGS = {
|
||||||
@@ -74,17 +71,11 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: { kind: 'number' },
|
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: { kind: 'number' },
|
||||||
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: { kind: 'number' },
|
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: { kind: 'number' },
|
||||||
[HCI_SETTING.SUPPORT_BUNDLE_FILE_NAME]: {
|
|
||||||
kind: 'string', canReset: true, featureFlag: 'supportBundleFileNameSetting'
|
|
||||||
},
|
|
||||||
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { kind: 'number', featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
|
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { kind: 'number', featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
|
||||||
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.STORAGE_NETWORK]: {
|
[HCI_SETTING.STORAGE_NETWORK]: {
|
||||||
kind: 'custom', from: 'import', canReset: true
|
kind: 'custom', from: 'import', canReset: true
|
||||||
},
|
},
|
||||||
[HCI_SETTING.RWX_NETWORK]: {
|
|
||||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'rwxNetworkSetting'
|
|
||||||
},
|
|
||||||
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.SSL_PARAMETERS]: {
|
[HCI_SETTING.SSL_PARAMETERS]: {
|
||||||
@@ -93,6 +84,7 @@ 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
|
||||||
},
|
},
|
||||||
@@ -125,18 +117,11 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
[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',
|
|
||||||
},
|
|
||||||
[HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: {
|
|
||||||
kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting'
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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: 'custom',
|
kind: 'url',
|
||||||
from: 'import',
|
|
||||||
canReset: true,
|
canReset: true,
|
||||||
},
|
},
|
||||||
[HCI_SETTING.UI_PL]: {
|
[HCI_SETTING.UI_PL]: {
|
||||||
|
|||||||
@@ -131,102 +131,3 @@ export const PROVIDER = {
|
|||||||
value: 'spec.provider',
|
value: 'spec.provider',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Source VM column in migration.harvesterhci.io.virtualmachineimport list page
|
|
||||||
export const VM_IMPORT_SOURCE_VM = {
|
|
||||||
name: 'sourceVm',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportSourceVm',
|
|
||||||
value: 'spec.virtualMachineName',
|
|
||||||
sort: 'spec.virtualMachineName',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Source Cluster column in migration.harvesterhci.io.virtualmachineimport list page
|
|
||||||
export const VM_IMPORT_SOURCE_CLUSTER = {
|
|
||||||
name: 'sourceCluster',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportSourceCluster',
|
|
||||||
value: 'spec.sourceCluster.name',
|
|
||||||
sort: 'spec.sourceCluster.name',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Import Status column in migration.harvesterhci.io.virtualmachineimport list page
|
|
||||||
export const VM_IMPORT_STATUS = {
|
|
||||||
name: 'importStatus',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportStatus',
|
|
||||||
value: 'status.importStatus',
|
|
||||||
sort: 'status.importStatus',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Datacenter column in migration.harvesterhci.io.vmwaresource list page
|
|
||||||
export const VM_IMPORT_SOURCE_V_DC = {
|
|
||||||
name: 'datacenter',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportSourceVDatacenter',
|
|
||||||
value: 'spec.dc',
|
|
||||||
sort: 'spec.dc',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Endpoint column in migration.harvesterhci.io.vmwaresource list page
|
|
||||||
export const VM_IMPORT_SOURCE_V_ENDPOINT = {
|
|
||||||
name: 'endpoint',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportSourceVEndpoint',
|
|
||||||
value: 'spec.endpoint',
|
|
||||||
sort: 'spec.endpoint',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cluster Status column in migration.harvesterhci.io.vmwaresource list page
|
|
||||||
export const VM_IMPORT_SOURCE_V_STATUS = {
|
|
||||||
name: 'clusterStatus',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportSourceVClusterStatus',
|
|
||||||
value: 'status.status',
|
|
||||||
sort: 'status.status',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Region column in migration.harvesterhci.io.openstacksource list page
|
|
||||||
export const VM_IMPORT_SOURCE_O_REGION = {
|
|
||||||
name: 'region',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportSourceORegion',
|
|
||||||
value: 'spec.region',
|
|
||||||
sort: 'spec.region',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Endpoint column in migration.harvesterhci.io.openstacksource list page
|
|
||||||
export const VM_IMPORT_SOURCE_O_ENDPOINT = {
|
|
||||||
name: 'endpoint',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportSourceOEndpoint',
|
|
||||||
value: 'spec.endpoint',
|
|
||||||
sort: 'spec.endpoint',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cluster Status column in migration.harvesterhci.io.openstacksource list page
|
|
||||||
export const VM_IMPORT_SOURCE_O_STATUS = {
|
|
||||||
name: 'clusterStatus',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportSourceOClusterStatus',
|
|
||||||
value: 'status.status',
|
|
||||||
sort: 'status.status',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// URL column in migration.harvesterhci.io.ovasource list page
|
|
||||||
export const VM_IMPORT_SOURCE_OVA_URL = {
|
|
||||||
name: 'url',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportSourceOVAUrl',
|
|
||||||
value: 'spec.url',
|
|
||||||
sort: 'spec.url',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Status column in migration.harvesterhci.io.ovasource list page
|
|
||||||
export const VM_IMPORT_SOURCE_OVA_STATUS = {
|
|
||||||
name: 'status',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportSourceOVAStatus',
|
|
||||||
value: 'status.status',
|
|
||||||
sort: 'status.status',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ 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 = {
|
||||||
@@ -24,25 +23,3 @@ export const INTERNAL_STORAGE_CLASS = {
|
|||||||
VMSTATE_PERSISTENCE: 'vmstate-persistence',
|
VMSTATE_PERSISTENCE: 'vmstate-persistence',
|
||||||
LONGHORN_STATIC: 'longhorn-static',
|
LONGHORN_STATIC: 'longhorn-static',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const L2VLAN_MODE = {
|
|
||||||
ACCESS: 'access',
|
|
||||||
TRUNK: 'trunk',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const VMIMPORT_SOURCE_PROVIDER = {
|
|
||||||
VMWARE: 'vmware',
|
|
||||||
OPENSTACK: 'openstack',
|
|
||||||
OVA: 'ova',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const VMIMPORT_SOURCE_KINDS = {
|
|
||||||
VMWARE: 'VmwareSource',
|
|
||||||
OPENSTACK: 'OpenstackSource',
|
|
||||||
OVA: 'OvaSource',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CDI_POPULATOR_KIND = {
|
|
||||||
VOLUME_IMPORT_SOURCE: 'VolumeImportSource',
|
|
||||||
VOLUME_CLONE_SOURCE: 'VolumeCloneSource',
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
<script>
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
|
||||||
import SortableTable from '@shell/components/SortableTable';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
|
|
||||||
components: {
|
|
||||||
ResourceTabs,
|
|
||||||
Tab,
|
|
||||||
SortableTable,
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
headers() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: 'profileName',
|
|
||||||
labelKey: 'harvester.migconfiguration.tableHeaders.profileName',
|
|
||||||
value: 'name',
|
|
||||||
width: 75,
|
|
||||||
sort: 'name',
|
|
||||||
dashIfEmpty: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'vGPUID',
|
|
||||||
labelKey: 'harvester.migconfiguration.tableHeaders.vGPUID',
|
|
||||||
value: 'vGPUID',
|
|
||||||
width: 75,
|
|
||||||
sort: 'vGPUID',
|
|
||||||
dashIfEmpty: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'available',
|
|
||||||
labelKey: 'harvester.migconfiguration.tableHeaders.available',
|
|
||||||
value: 'available',
|
|
||||||
width: 75,
|
|
||||||
sort: 'available',
|
|
||||||
align: 'center',
|
|
||||||
dashIfEmpty: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'requested',
|
|
||||||
labelKey: 'harvester.migconfiguration.tableHeaders.requested',
|
|
||||||
value: 'requested',
|
|
||||||
width: 75,
|
|
||||||
sort: 'requested',
|
|
||||||
align: 'center',
|
|
||||||
dashIfEmpty: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'total',
|
|
||||||
labelKey: 'harvester.migconfiguration.tableHeaders.total',
|
|
||||||
value: 'total',
|
|
||||||
width: 75,
|
|
||||||
sort: 'total',
|
|
||||||
align: 'center',
|
|
||||||
dashIfEmpty: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
rows() {
|
|
||||||
let out = (this.value?.status?.profileStatus || []).map((profile) => {
|
|
||||||
const {
|
|
||||||
id, name, total, available
|
|
||||||
} = profile;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
total,
|
|
||||||
available,
|
|
||||||
vGPUID: profile.vGPUID?.join(', ') || '',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
out = out.map((row) => {
|
|
||||||
const requested = this.value?.spec?.profileSpec.find((p) => p.id === row.id)?.requested || 0;
|
|
||||||
|
|
||||||
return { ...row, requested };
|
|
||||||
});
|
|
||||||
|
|
||||||
return out;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ResourceTabs
|
|
||||||
:value="value"
|
|
||||||
:need-events="false"
|
|
||||||
:need-related="false"
|
|
||||||
:mode="mode"
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
name="Profile Status"
|
|
||||||
:label="t('harvester.migconfiguration.profileStatus')"
|
|
||||||
>
|
|
||||||
<SortableTable
|
|
||||||
:headers="headers"
|
|
||||||
:rows="rows"
|
|
||||||
key-field="condition"
|
|
||||||
default-sort-by="condition"
|
|
||||||
:table-actions="false"
|
|
||||||
:row-actions="false"
|
|
||||||
:search="false"
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
</ResourceTabs>
|
|
||||||
</template>
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
<script>
|
|
||||||
import LabelValue from '@shell/components/LabelValue';
|
|
||||||
import { HCI } from '../../types';
|
|
||||||
import { DOC } from '../../config/doc-links';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterHugepages',
|
|
||||||
components: { LabelValue },
|
|
||||||
|
|
||||||
props: {
|
|
||||||
node: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
docsTransparentHugepagesLink() {
|
|
||||||
return DOC.TRANSPARENT_HUGEPAGES;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
|
|
||||||
|
|
||||||
this.hugepages = hash.find((node) => {
|
|
||||||
return node.id === this.node.id;
|
|
||||||
}) || {};
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return { hugepages: {} };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<template v-if="hugepages.status">
|
|
||||||
<h2>{{ t('harvester.host.hugepages.meminfo') }}</h2>
|
|
||||||
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabelValue
|
|
||||||
:name="t('harvester.host.hugepages.status.anon')"
|
|
||||||
:value="hugepages.status.meminfo.anonHugePages"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabelValue
|
|
||||||
:name="t('harvester.host.hugepages.status.size')"
|
|
||||||
:value="hugepages.status.meminfo.hugepageSize"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-3">
|
|
||||||
<LabelValue
|
|
||||||
:name="t('harvester.host.hugepages.status.total')"
|
|
||||||
:value="hugepages.status.meminfo.hugePagesTotal"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-3">
|
|
||||||
<LabelValue
|
|
||||||
:name="t('harvester.host.hugepages.status.free')"
|
|
||||||
:value="hugepages.status.meminfo.hugePagesFree"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-3">
|
|
||||||
<LabelValue
|
|
||||||
:name="t('harvester.host.hugepages.status.rsvd')"
|
|
||||||
:value="hugepages.status.meminfo.hugePagesRsvd"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-3">
|
|
||||||
<LabelValue
|
|
||||||
:name="t('harvester.host.hugepages.status.surp')"
|
|
||||||
:value="hugepages.status.meminfo.hugePagesSurp"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<hr class="divider" />
|
|
||||||
<h3>
|
|
||||||
<t
|
|
||||||
k="harvester.host.hugepages.transparent.title"
|
|
||||||
:raw="true"
|
|
||||||
:url="docsTransparentHugepagesLink"
|
|
||||||
/>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-4">
|
|
||||||
<LabelValue
|
|
||||||
:name="t('harvester.host.hugepages.transparent.enabled')"
|
|
||||||
:value="hugepages.spec.transparent.enabled"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-4">
|
|
||||||
<LabelValue
|
|
||||||
:name="t('harvester.host.hugepages.transparent.shmemEnabled')"
|
|
||||||
:value="hugepages.spec.transparent.shmemEnabled"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-4">
|
|
||||||
<LabelValue
|
|
||||||
:name="t('harvester.host.hugepages.transparent.defrag')"
|
|
||||||
:value="hugepages.spec.transparent.defrag"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -27,7 +27,6 @@ import Instance from './VirtualMachineInstance';
|
|||||||
import Disk from './HarvesterHostDisk';
|
import 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';
|
||||||
@@ -47,7 +46,6 @@ export default {
|
|||||||
VlanStatus,
|
VlanStatus,
|
||||||
LabelValue,
|
LabelValue,
|
||||||
HarvesterKsmtuned,
|
HarvesterKsmtuned,
|
||||||
HarvesterHugepages,
|
|
||||||
Loading,
|
Loading,
|
||||||
SortableTable,
|
SortableTable,
|
||||||
HarvesterSeeder,
|
HarvesterSeeder,
|
||||||
@@ -211,12 +209,6 @@ 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);
|
||||||
},
|
},
|
||||||
@@ -476,16 +468,6 @@ 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"
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export default {
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col span-6 mb-20">
|
<div class="col span-6 mb-20">
|
||||||
<LabelValue
|
<LabelValue
|
||||||
:name="t('harvester.schedule.cron.label')"
|
:name="t('harvester.schedule.cron')"
|
||||||
:value="cronExpression"
|
:value="cronExpression"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -213,7 +213,6 @@ 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
|
||||||
}
|
}
|
||||||
@@ -266,7 +265,6 @@ export default {
|
|||||||
<Network
|
<Network
|
||||||
v-model:value="networkRows"
|
v-model:value="networkRows"
|
||||||
mode="view"
|
mode="view"
|
||||||
:vm="value"
|
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
|||||||
@@ -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,7 +145,6 @@ 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();
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { Card } from '@components/Card';
|
|||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
import { escapeHtml } from '@shell/utils/string';
|
import { escapeHtml } from '@shell/utils/string';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import { getHarvesterUserName } from '../utils/auth';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEnablePciPassthrough',
|
name: 'HarvesterEnablePciPassthrough',
|
||||||
@@ -35,7 +34,16 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async save(buttonCb) {
|
async save(buttonCb) {
|
||||||
const userName = getHarvesterUserName(this.$store.getters);
|
// isSingleProduct == this is a standalone Harvester cluster
|
||||||
|
const isSingleProduct = this.$store.getters['isSingleProduct'];
|
||||||
|
let userName = 'admin';
|
||||||
|
|
||||||
|
// if this is imported Harvester, there may be users other than 'admin
|
||||||
|
if (!isSingleProduct) {
|
||||||
|
const user = this.$store.getters['auth/v3User'];
|
||||||
|
|
||||||
|
userName = user?.username || user?.id;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < this.resources.length; i++) {
|
for (let i = 0; i < this.resources.length; i++) {
|
||||||
const actionResource = this.resources[i];
|
const actionResource = this.resources[i];
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { Card } from '@components/Card';
|
|||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
import { escapeHtml } from '@shell/utils/string';
|
import { escapeHtml } from '@shell/utils/string';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import { getHarvesterUserName } from '../utils/auth';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEnableUSBPassthrough',
|
name: 'HarvesterEnableUSBPassthrough',
|
||||||
@@ -35,7 +34,16 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async save(buttonCb) {
|
async save(buttonCb) {
|
||||||
const userName = getHarvesterUserName(this.$store.getters);
|
// isSingleProduct == this is a standalone Harvester cluster
|
||||||
|
const isSingleProduct = this.$store.getters['isSingleProduct'];
|
||||||
|
let userName = 'admin';
|
||||||
|
|
||||||
|
// if this is imported Harvester, there may be users other than 'admin
|
||||||
|
if (!isSingleProduct) {
|
||||||
|
const user = this.$store.getters['auth/v3User'];
|
||||||
|
|
||||||
|
userName = user?.username || user?.id;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < this.resources.length; i++) {
|
for (let i = 0; i < this.resources.length; i++) {
|
||||||
const actionResource = this.resources[i];
|
const actionResource = this.resources[i];
|
||||||
|
|||||||
+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: 'HotplugVolumeModal',
|
name: 'HotplugModal',
|
||||||
|
|
||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return !pvc.attachVM;
|
||||||
})
|
})
|
||||||
.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.hotplugVolume.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
|
message: this.t('harvester.modal.hotplug.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
|
||||||
}, { root: true });
|
}, { root: true });
|
||||||
|
|
||||||
this.close();
|
this.close();
|
||||||
@@ -122,7 +122,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<h4
|
<h4
|
||||||
v-clean-html="t('harvester.modal.hotplugVolume.title')"
|
v-clean-html="t('harvester.modal.hotplug.title')"
|
||||||
class="text-default-text"
|
class="text-default-text"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
|
||||||
import { Card } from '@components/Card';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
|
||||||
import { NETWORK_TYPE } from '../config/types';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'AddHotplugNic',
|
|
||||||
|
|
||||||
emits: ['close'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
AsyncButton,
|
|
||||||
Card,
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
Banner
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
resources: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
try {
|
|
||||||
this.allVMNetworks = await this.$store.dispatch('harvester/findAll', { type: NETWORK_ATTACHMENT });
|
|
||||||
} catch (err) {
|
|
||||||
this.errors = exceptionToErrorsArray(err);
|
|
||||||
this.allVMNetworks = [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
interfaceName: '',
|
|
||||||
networkName: '',
|
|
||||||
macAddress: '',
|
|
||||||
allVMNetworks: [],
|
|
||||||
errors: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
actionResource() {
|
|
||||||
return this.resources?.[0];
|
|
||||||
},
|
|
||||||
|
|
||||||
isFormValid() {
|
|
||||||
return this.interfaceName !== '' && this.networkName !== '';
|
|
||||||
},
|
|
||||||
|
|
||||||
vmNetworksOption() {
|
|
||||||
return this.allVMNetworks
|
|
||||||
.filter((network) => {
|
|
||||||
const labels = network.metadata?.labels || {};
|
|
||||||
const type = labels[HCI_ANNOTATIONS.NETWORK_TYPE];
|
|
||||||
|
|
||||||
const isValidType = [
|
|
||||||
NETWORK_TYPE.L2VLAN,
|
|
||||||
NETWORK_TYPE.UNTAGGED,
|
|
||||||
NETWORK_TYPE.L2TRUNK_VLAN,
|
|
||||||
].includes(type);
|
|
||||||
|
|
||||||
return isValidType && !network.isSystem;
|
|
||||||
})
|
|
||||||
.map((network) => {
|
|
||||||
const label = network.isNotReady ? `${ network.id } (${ this.t('generic.notReady') })` : network.id;
|
|
||||||
|
|
||||||
return ({
|
|
||||||
label,
|
|
||||||
value: network.id || '',
|
|
||||||
disabled: network.isNotReady,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.interfaceName = '';
|
|
||||||
this.networkName = '';
|
|
||||||
this.macAddress = '';
|
|
||||||
this.errors = [];
|
|
||||||
this.$emit('close');
|
|
||||||
},
|
|
||||||
|
|
||||||
async save(buttonCb) {
|
|
||||||
if (!this.actionResource) {
|
|
||||||
buttonCb(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
interfaceName: this.interfaceName,
|
|
||||||
networkName: this.networkName
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.macAddress) {
|
|
||||||
payload.macAddress = this.macAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await this.actionResource.doAction('addNic', payload);
|
|
||||||
|
|
||||||
if ([200, 204].includes(res?._status)) {
|
|
||||||
this.$store.dispatch('growl/success', {
|
|
||||||
title: this.t('generic.notification.title.succeed'),
|
|
||||||
message: this.t('harvester.modal.hotplugNic.success', {
|
|
||||||
interfaceName: this.interfaceName,
|
|
||||||
vm: this.actionResource.nameDisplay
|
|
||||||
})
|
|
||||||
}, { root: true });
|
|
||||||
|
|
||||||
this.close();
|
|
||||||
buttonCb(true);
|
|
||||||
} else {
|
|
||||||
this.errors = exceptionToErrorsArray(res);
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.errors = exceptionToErrorsArray(err);
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Card
|
|
||||||
ref="modal"
|
|
||||||
name="modal"
|
|
||||||
:show-highlight-border="false"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<h4
|
|
||||||
v-clean-html="t('harvester.modal.hotplugNic.title')"
|
|
||||||
class="text-default-text"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="interfaceName"
|
|
||||||
:label="t('generic.name')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="networkName"
|
|
||||||
class="mt-20"
|
|
||||||
:label="t('harvester.modal.hotplugNic.vmNetwork')"
|
|
||||||
:options="vmNetworksOption"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="macAddress"
|
|
||||||
class="mt-20"
|
|
||||||
label-key="harvester.modal.hotplugNic.macAddress"
|
|
||||||
:tooltip="t('harvester.modal.hotplugNic.macAddressTooltip', _, true)"
|
|
||||||
/>
|
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
:label="err"
|
|
||||||
color="error"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #actions>
|
|
||||||
<div class="actions">
|
|
||||||
<div class="buttons">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn role-secondary mr-10"
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
{{ t('generic.cancel') }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<AsyncButton
|
|
||||||
mode="apply"
|
|
||||||
:disabled="!isFormValid"
|
|
||||||
@click="save"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
import { STORAGE_CLASS } from '@shell/config/types';
|
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
|
||||||
import { sortBy } from '@shell/utils/sort';
|
|
||||||
import { isInternalStorageClass } from '../utils/storage-class';
|
|
||||||
|
|
||||||
import { Card } from '@components/Card';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterDataMigrationDialog',
|
|
||||||
|
|
||||||
emits: ['close'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
AsyncButton, Banner, Card, LabeledInput, LabeledSelect
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
resources: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
this.storageClasses = await this.$store.dispatch('harvester/findAll', { type: STORAGE_CLASS });
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
targetVolumeName: '',
|
|
||||||
targetStorageClassName: '',
|
|
||||||
errors: [],
|
|
||||||
storageClasses: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
actionResource() {
|
|
||||||
return this.resources[0];
|
|
||||||
},
|
|
||||||
|
|
||||||
storageClassOptions() {
|
|
||||||
return sortBy(
|
|
||||||
this.storageClasses
|
|
||||||
.filter((sc) => !isInternalStorageClass(sc.metadata?.name))
|
|
||||||
.map((sc) => ({
|
|
||||||
label: sc.metadata?.name,
|
|
||||||
value: sc.metadata?.name
|
|
||||||
})),
|
|
||||||
'label'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
disableSave() {
|
|
||||||
return !this.targetVolumeName || !this.targetStorageClassName;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.targetVolumeName = '';
|
|
||||||
this.targetStorageClassName = '';
|
|
||||||
this.errors = [];
|
|
||||||
this.$emit('close');
|
|
||||||
},
|
|
||||||
|
|
||||||
async apply(buttonDone) {
|
|
||||||
if (!this.actionResource) {
|
|
||||||
buttonDone(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.targetVolumeName) {
|
|
||||||
const name = this.t('harvester.modal.dataMigration.fields.targetVolumeName.label');
|
|
||||||
|
|
||||||
this['errors'] = [this.t('validation.required', { key: name })];
|
|
||||||
buttonDone(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.targetStorageClassName) {
|
|
||||||
const name = this.t('harvester.modal.dataMigration.fields.targetStorageClassName.label');
|
|
||||||
|
|
||||||
this['errors'] = [this.t('validation.required', { key: name })];
|
|
||||||
buttonDone(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.actionResource.doAction('dataMigration', {
|
|
||||||
targetVolumeName: this.targetVolumeName,
|
|
||||||
targetStorageClassName: this.targetStorageClassName
|
|
||||||
}, {}, false);
|
|
||||||
|
|
||||||
buttonDone(true);
|
|
||||||
this.close();
|
|
||||||
} catch (err) {
|
|
||||||
const error = err?.data || err;
|
|
||||||
|
|
||||||
this['errors'] = exceptionToErrorsArray(error);
|
|
||||||
buttonDone(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Card :show-highlight-border="false">
|
|
||||||
<template #title>
|
|
||||||
{{ t('harvester.modal.dataMigration.title') }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="targetVolumeName"
|
|
||||||
:label="t('harvester.modal.dataMigration.fields.targetVolumeName.label')"
|
|
||||||
:placeholder="t('harvester.modal.dataMigration.fields.targetVolumeName.placeholder')"
|
|
||||||
class="mb-20"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="targetStorageClassName"
|
|
||||||
:label="t('harvester.modal.dataMigration.fields.targetStorageClassName.label')"
|
|
||||||
:placeholder="t('harvester.modal.dataMigration.fields.targetStorageClassName.placeholder')"
|
|
||||||
:options="storageClassOptions"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template
|
|
||||||
#actions
|
|
||||||
class="actions"
|
|
||||||
>
|
|
||||||
<div class="buttons">
|
|
||||||
<button
|
|
||||||
class="btn role-secondary mr-10"
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
{{ t('generic.cancel') }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<AsyncButton
|
|
||||||
mode="apply"
|
|
||||||
:disabled="disableSave"
|
|
||||||
@click="apply"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
|
||||||
import { mapState, mapGetters } from 'vuex';
|
|
||||||
import { Card } from '@components/Card';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
|
||||||
import { HCI } from '../types';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterInsertCdRomVolume',
|
|
||||||
|
|
||||||
emits: ['close'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
AsyncButton,
|
|
||||||
Card,
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
Banner
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
resources: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
try {
|
|
||||||
this.images = await this.$store.dispatch('harvester/findAll', { type: HCI.IMAGE });
|
|
||||||
} catch (err) {
|
|
||||||
this.errors = exceptionToErrorsArray(err);
|
|
||||||
this.images = [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
imageName: '',
|
|
||||||
images: [],
|
|
||||||
errors: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapState('action-menu', ['modalData']),
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
actionResource() {
|
|
||||||
return this.resources?.[0];
|
|
||||||
},
|
|
||||||
|
|
||||||
isFormValid() {
|
|
||||||
return this.imageName !== '';
|
|
||||||
},
|
|
||||||
|
|
||||||
deviceName() {
|
|
||||||
return this.modalData.name;
|
|
||||||
},
|
|
||||||
|
|
||||||
imagesOption() {
|
|
||||||
return this.images
|
|
||||||
.filter((image) => {
|
|
||||||
const labels = image.metadata?.labels || {};
|
|
||||||
const type = labels[HCI_ANNOTATIONS.IMAGE_SUFFIX];
|
|
||||||
|
|
||||||
return type === 'iso';
|
|
||||||
})
|
|
||||||
.map((image) => {
|
|
||||||
return ({
|
|
||||||
label: this.imageOptionLabel(image),
|
|
||||||
value: image.id,
|
|
||||||
disabled: image.isImportedImage
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.imageName = '';
|
|
||||||
this.errors = [];
|
|
||||||
this.$emit('close');
|
|
||||||
},
|
|
||||||
|
|
||||||
imageOptionLabel(image) {
|
|
||||||
return `${ image.metadata.namespace }/${ image.spec.displayName }`;
|
|
||||||
},
|
|
||||||
|
|
||||||
async save(buttonCb) {
|
|
||||||
if (!this.actionResource) {
|
|
||||||
buttonCb(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
deviceName: this.deviceName,
|
|
||||||
imageName: this.imageName
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await this.actionResource.doAction('insertCdRomVolume', payload);
|
|
||||||
|
|
||||||
if ([200, 204].includes(res?._status)) {
|
|
||||||
this.$store.dispatch('growl/success', {
|
|
||||||
title: this.t('generic.notification.title.succeed'),
|
|
||||||
message: this.t('harvester.modal.insertCdRomVolume.success', {
|
|
||||||
deviceName: this.deviceName,
|
|
||||||
imageName: this.imageName,
|
|
||||||
})
|
|
||||||
}, { root: true });
|
|
||||||
|
|
||||||
this.close();
|
|
||||||
buttonCb(true);
|
|
||||||
} else {
|
|
||||||
this.errors = exceptionToErrorsArray(res);
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.errors = exceptionToErrorsArray(err);
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Card
|
|
||||||
ref="modal"
|
|
||||||
name="modal"
|
|
||||||
:show-highlight-border="false"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<h4
|
|
||||||
v-clean-html="t('harvester.modal.insertCdRomVolume.title')"
|
|
||||||
class="text-default-text"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="deviceName"
|
|
||||||
:label="t('generic.name')"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="imageName"
|
|
||||||
class="mt-20"
|
|
||||||
:label="t('harvester.modal.insertCdRomVolume.image')"
|
|
||||||
:options="imagesOption"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
:label="err"
|
|
||||||
color="error"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #actions>
|
|
||||||
<div class="actions">
|
|
||||||
<div class="buttons">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn role-secondary mr-10"
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
{{ t('generic.cancel') }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<AsyncButton
|
|
||||||
mode="apply"
|
|
||||||
:disabled="!isFormValid"
|
|
||||||
@click="save"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
import { PVC } from '@shell/config/types';
|
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
|
||||||
import { sortBy } from '@shell/utils/sort';
|
|
||||||
import { HCI } from '../types';
|
|
||||||
import { parseVolumeClaimTemplates } from '@pkg/harvester/utils/vm';
|
|
||||||
|
|
||||||
import { Card } from '@components/Card';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterStorageMigrationDialog',
|
|
||||||
|
|
||||||
emits: ['close'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
AsyncButton, Banner, Card, LabeledSelect
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
resources: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
this.allPVCs = await this.$store.dispatch('harvester/findAll', { type: PVC });
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
sourceVolume: '',
|
|
||||||
targetVolume: '',
|
|
||||||
errors: [],
|
|
||||||
allPVCs: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
actionResource() {
|
|
||||||
return this.resources[0];
|
|
||||||
},
|
|
||||||
|
|
||||||
sourceVolumeOptions() {
|
|
||||||
const volumes = this.actionResource.spec?.template?.spec?.volumes || [];
|
|
||||||
|
|
||||||
return sortBy(
|
|
||||||
volumes
|
|
||||||
.map((v) => v.persistentVolumeClaim?.claimName)
|
|
||||||
.filter((name) => !!name)
|
|
||||||
.map((name) => ({
|
|
||||||
label: name,
|
|
||||||
value: name
|
|
||||||
})),
|
|
||||||
'label'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
namespacePVCs() {
|
|
||||||
return this.allPVCs.filter((pvc) => pvc.metadata.namespace === this.actionResource.metadata.namespace);
|
|
||||||
},
|
|
||||||
|
|
||||||
vmUsedVolumeNames() {
|
|
||||||
const allVMs = this.$store.getters['harvester/all'](HCI.VM) || [];
|
|
||||||
const names = new Set();
|
|
||||||
|
|
||||||
allVMs.forEach((vm) => {
|
|
||||||
// Collect volume names from spec.template.spec.volumes (both PVC and DataVolume references)
|
|
||||||
const volumes = vm.spec?.template?.spec?.volumes || [];
|
|
||||||
|
|
||||||
volumes.forEach((v) => {
|
|
||||||
const name = v.persistentVolumeClaim?.claimName || v.dataVolume?.name;
|
|
||||||
|
|
||||||
if (name) {
|
|
||||||
names.add(`${ vm.metadata.namespace }/${ name }`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Collect volume names from volumeClaimTemplates annotation
|
|
||||||
const templates = parseVolumeClaimTemplates(vm);
|
|
||||||
|
|
||||||
templates.forEach((t) => {
|
|
||||||
if (t.metadata?.name) {
|
|
||||||
names.add(`${ vm.metadata.namespace }/${ t.metadata.name }`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return names;
|
|
||||||
},
|
|
||||||
|
|
||||||
targetVolumeOptions() {
|
|
||||||
return sortBy(
|
|
||||||
this.namespacePVCs
|
|
||||||
.filter((pvc) => {
|
|
||||||
// Exclude volumes used by any VM (via spec.volumes or volumeClaimTemplates)
|
|
||||||
if (this.vmUsedVolumeNames.has(`${ pvc.metadata.namespace }/${ pvc.metadata.name }`)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.map((pvc) => ({
|
|
||||||
label: pvc.metadata.name,
|
|
||||||
value: pvc.metadata.name
|
|
||||||
})),
|
|
||||||
'label'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
disableSave() {
|
|
||||||
return !this.sourceVolume || !this.targetVolume;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.sourceVolume = '';
|
|
||||||
this.targetVolume = '';
|
|
||||||
this.errors = [];
|
|
||||||
this.$emit('close');
|
|
||||||
},
|
|
||||||
|
|
||||||
async apply(buttonDone) {
|
|
||||||
if (!this.actionResource) {
|
|
||||||
buttonDone(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.sourceVolume) {
|
|
||||||
const name = this.t('harvester.modal.storageMigration.fields.sourceVolume.label');
|
|
||||||
|
|
||||||
this['errors'] = [this.t('validation.required', { key: name })];
|
|
||||||
buttonDone(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.targetVolume) {
|
|
||||||
const name = this.t('harvester.modal.storageMigration.fields.targetVolume.label');
|
|
||||||
|
|
||||||
this['errors'] = [this.t('validation.required', { key: name })];
|
|
||||||
buttonDone(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.actionResource.doAction('storageMigration', {
|
|
||||||
sourceVolume: this.sourceVolume,
|
|
||||||
targetVolume: this.targetVolume
|
|
||||||
}, {}, false);
|
|
||||||
|
|
||||||
buttonDone(true);
|
|
||||||
this.close();
|
|
||||||
} catch (err) {
|
|
||||||
const error = err?.data || err;
|
|
||||||
|
|
||||||
this['errors'] = exceptionToErrorsArray(error);
|
|
||||||
buttonDone(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Card :show-highlight-border="false">
|
|
||||||
<template #title>
|
|
||||||
{{ t('harvester.modal.storageMigration.title') }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="sourceVolume"
|
|
||||||
:label="t('harvester.modal.storageMigration.fields.sourceVolume.label')"
|
|
||||||
:placeholder="t('harvester.modal.storageMigration.fields.sourceVolume.placeholder')"
|
|
||||||
:options="sourceVolumeOptions"
|
|
||||||
class="mb-20"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="targetVolume"
|
|
||||||
:label="t('harvester.modal.storageMigration.fields.targetVolume.label')"
|
|
||||||
:placeholder="t('harvester.modal.storageMigration.fields.targetVolume.placeholder')"
|
|
||||||
:options="targetVolumeOptions"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template
|
|
||||||
#actions
|
|
||||||
class="actions"
|
|
||||||
>
|
|
||||||
<div class="buttons">
|
|
||||||
<button
|
|
||||||
class="btn role-secondary mr-10"
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
{{ t('generic.cancel') }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<AsyncButton
|
|
||||||
mode="apply"
|
|
||||||
:disabled="disableSave"
|
|
||||||
@click="apply"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -277,9 +277,8 @@ export default {
|
|||||||
:label="t('harvester.modal.bundle.namespaces.label')"
|
:label="t('harvester.modal.bundle.namespaces.label')"
|
||||||
:clearable="true"
|
:clearable="true"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
:append-to-body="false"
|
|
||||||
:options="namespaceOptions"
|
:options="namespaceOptions"
|
||||||
class="mb-10 namespace-select"
|
class="mb-10 label-select"
|
||||||
:tooltip="t('harvester.modal.bundle.namespaces.tooltip', _, true)"
|
:tooltip="t('harvester.modal.bundle.namespaces.tooltip', _, true)"
|
||||||
@update:value="updateNamespaces"
|
@update:value="updateNamespaces"
|
||||||
/>
|
/>
|
||||||
@@ -373,11 +372,6 @@ export default {
|
|||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
height: 160px;
|
height: 160px;
|
||||||
}
|
}
|
||||||
.namespace-select {
|
|
||||||
:deep(.vs__dropdown-menu) {
|
|
||||||
max-height: 210px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
|
|||||||
+12
-62
@@ -1,16 +1,13 @@
|
|||||||
<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';
|
||||||
|
|
||||||
const VOLUME = 'volume';
|
|
||||||
const NETWORK = 'network';
|
|
||||||
const CDROM = 'cdrom';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterHotUnplug',
|
name: 'HarvesterHotUnplugModal',
|
||||||
|
|
||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
|
|
||||||
@@ -38,43 +35,8 @@ export default {
|
|||||||
actionResource() {
|
actionResource() {
|
||||||
return this.resources[0];
|
return this.resources[0];
|
||||||
},
|
},
|
||||||
|
diskName() {
|
||||||
name() {
|
return this.modalData.diskName;
|
||||||
return this.modalData.name;
|
|
||||||
},
|
|
||||||
|
|
||||||
isVolume() {
|
|
||||||
return this.modalData.type === VOLUME;
|
|
||||||
},
|
|
||||||
|
|
||||||
titleKey() {
|
|
||||||
const keys = {
|
|
||||||
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.title',
|
|
||||||
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.title',
|
|
||||||
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.title',
|
|
||||||
};
|
|
||||||
|
|
||||||
return keys[this.modalData.type];
|
|
||||||
},
|
|
||||||
|
|
||||||
actionLabelKey() {
|
|
||||||
const keys = {
|
|
||||||
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabels',
|
|
||||||
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.actionLabels',
|
|
||||||
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.actionLabels',
|
|
||||||
};
|
|
||||||
|
|
||||||
return keys[this.modalData.type];
|
|
||||||
},
|
|
||||||
|
|
||||||
successMessageKey() {
|
|
||||||
const keys = {
|
|
||||||
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.success',
|
|
||||||
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.success',
|
|
||||||
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.success',
|
|
||||||
};
|
|
||||||
|
|
||||||
return keys[this.modalData.type];
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -85,22 +47,14 @@ export default {
|
|||||||
|
|
||||||
async save(buttonCb) {
|
async save(buttonCb) {
|
||||||
try {
|
try {
|
||||||
let res;
|
const res = await this.actionResource.doAction('removeVolume', { diskName: this.diskName });
|
||||||
|
|
||||||
if (this.modalData.type === VOLUME) {
|
|
||||||
res = await this.actionResource.doAction('removeVolume', { diskName: this.name });
|
|
||||||
} else if (this.modalData.type === NETWORK) {
|
|
||||||
res = await this.actionResource.doAction('removeNic', { interfaceName: this.name });
|
|
||||||
} else {
|
|
||||||
res = await this.actionResource.doAction('ejectCdRomVolume', { deviceName: this.name });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res._status === 200 || res._status === 204) {
|
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(this.successMessageKey, { name: this.name })
|
message: this.t('harvester.modal.hotunplug.success', { name: this.diskName })
|
||||||
},
|
},
|
||||||
{ root: true }
|
{ root: true }
|
||||||
);
|
);
|
||||||
@@ -110,14 +64,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,7 +87,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<h4
|
<h4
|
||||||
v-clean-html="t(titleKey, { name })"
|
v-clean-html="t('harvester.virtualMachine.unplug.title', { name: diskName })"
|
||||||
class="text-default-text"
|
class="text-default-text"
|
||||||
/>
|
/>
|
||||||
<Banner
|
<Banner
|
||||||
@@ -157,9 +111,9 @@ export default {
|
|||||||
|
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
mode="apply"
|
mode="apply"
|
||||||
:action-label="t(actionLabelKey)"
|
:action-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
||||||
:waiting-label="t(actionLabelKey)"
|
:waiting-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
||||||
:success-label="t(actionLabelKey)"
|
:success-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
||||||
@click="save"
|
@click="save"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -178,8 +132,4 @@ export default {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep(.card-title) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
<script>
|
|
||||||
import Tabbed from '@shell/components/Tabbed';
|
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
|
||||||
import CruResource from '@shell/components/CruResource';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|
||||||
import LabelValue from '@shell/components/LabelValue';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterEditMIGConfiguration',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
Tab,
|
|
||||||
Tabbed,
|
|
||||||
CruResource,
|
|
||||||
LabeledInput,
|
|
||||||
NameNsDescription,
|
|
||||||
LabelValue
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
|
||||||
|
|
||||||
inheritAttrs: false,
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
const { profileSpec } = this.value.spec;
|
|
||||||
|
|
||||||
return { profileSpec: profileSpec || [] };
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
if (this.registerBeforeHook) {
|
|
||||||
this.registerBeforeHook(this.updateBeforeSave);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
isView() {
|
|
||||||
return this.mode === 'view';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updateBeforeSave() {
|
|
||||||
// MIGConfiguration CRD don't have any namespace field,
|
|
||||||
// so we need to remove the namespace field before saving
|
|
||||||
delete this.value.metadata.namespace;
|
|
||||||
// enable the MIGConfiguration when saving
|
|
||||||
this.value.spec.enabled = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
labelTitle(profile) {
|
|
||||||
return `${ profile.name } (available : ${ this.available(profile) })`;
|
|
||||||
},
|
|
||||||
|
|
||||||
available(profile) {
|
|
||||||
const count = this.value.status?.profileStatus?.find((p) => p.id === profile.id)?.available;
|
|
||||||
|
|
||||||
return count || 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
updateRequested(neu, profile) {
|
|
||||||
if (neu === null || neu === '') return;
|
|
||||||
const newValue = Number(neu);
|
|
||||||
const availableCount = this.available(profile);
|
|
||||||
|
|
||||||
if (newValue < 0) {
|
|
||||||
profile.requested = 0;
|
|
||||||
} else if ( newValue > availableCount ) {
|
|
||||||
profile.requested = availableCount;
|
|
||||||
} else {
|
|
||||||
profile.requested = newValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CruResource
|
|
||||||
:done-route="doneRoute"
|
|
||||||
:resource="value"
|
|
||||||
:mode="mode"
|
|
||||||
:errors="errors"
|
|
||||||
:apply-hooks="applyHooks"
|
|
||||||
finish-button-mode="enable"
|
|
||||||
@finish="save"
|
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
|
||||||
<NameNsDescription
|
|
||||||
:value="value"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
<Tabbed
|
|
||||||
v-bind="$attrs"
|
|
||||||
class="mt-15"
|
|
||||||
:side-tabs="true"
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
name="profileSpec"
|
|
||||||
:label="t('harvester.migconfiguration.profileSpec')"
|
|
||||||
:weight="1"
|
|
||||||
class="bordered-table"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="(profile, index) in profileSpec"
|
|
||||||
:key="index"
|
|
||||||
>
|
|
||||||
<LabelValue
|
|
||||||
:value="labelTitle(profile)"
|
|
||||||
class="mb-10"
|
|
||||||
/>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="profile.requested"
|
|
||||||
:min="0"
|
|
||||||
:disabled="isView"
|
|
||||||
type="number"
|
|
||||||
class="mb-20"
|
|
||||||
:label="`${t('harvester.migconfiguration.requested')}`"
|
|
||||||
@update:value="updateRequested($event, profile)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
</Tabbed>
|
|
||||||
</CruResource>
|
|
||||||
</template>
|
|
||||||
@@ -8,7 +8,7 @@ import LabelValue from '@shell/components/LabelValue';
|
|||||||
import { BadgeState } from '@components/BadgeState';
|
import { 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 } from '@components/Form/Radio';
|
import { RadioGroup, RadioButton } 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,6 +30,7 @@ export default {
|
|||||||
BadgeState,
|
BadgeState,
|
||||||
Banner,
|
Banner,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
|
RadioButton,
|
||||||
ModalWithCard,
|
ModalWithCard,
|
||||||
Tags,
|
Tags,
|
||||||
},
|
},
|
||||||
@@ -183,11 +184,12 @@ 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 (this.blockDevice?.childParts?.length > 0) {
|
if (lastFormattedAt || this.blockDevice?.childParts?.length > 0) {
|
||||||
return true;
|
return true;
|
||||||
} else if (systems.includes(fileSystem)) {
|
} else if (systems.includes(fileSystem)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -444,7 +446,7 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="(value.isNew && isLonghornV1) || isCorrupted"
|
v-if="(value.isNew && isLonghornV1 && !isFormatted) || isCorrupted"
|
||||||
class="col span-6"
|
class="col span-6"
|
||||||
>
|
>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
@@ -457,6 +459,15 @@ 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
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
<script>
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
|
||||||
import { HCI } from '../../types';
|
|
||||||
import { DOC } from '../../config/doc-links';
|
|
||||||
|
|
||||||
export const hugepagesTHPEnabledMode = [{
|
|
||||||
label: 'Always',
|
|
||||||
value: 'always',
|
|
||||||
}, {
|
|
||||||
label: 'Madvise',
|
|
||||||
value: 'madvise',
|
|
||||||
}, {
|
|
||||||
label: 'Never',
|
|
||||||
value: 'never',
|
|
||||||
}];
|
|
||||||
|
|
||||||
export const hugepagesTHPShmemEnabledMode = [{
|
|
||||||
label: 'Always',
|
|
||||||
value: 'always',
|
|
||||||
}, {
|
|
||||||
label: 'Within Size',
|
|
||||||
value: 'within_size',
|
|
||||||
}, {
|
|
||||||
label: 'Advise',
|
|
||||||
value: 'advise',
|
|
||||||
}, {
|
|
||||||
label: 'Never',
|
|
||||||
value: 'never',
|
|
||||||
}, {
|
|
||||||
label: 'Deny',
|
|
||||||
value: 'deny',
|
|
||||||
}, {
|
|
||||||
label: 'Force',
|
|
||||||
value: 'force',
|
|
||||||
}];
|
|
||||||
|
|
||||||
export const hugepagesTHPDefragMode = [{
|
|
||||||
label: 'Always',
|
|
||||||
value: 'always',
|
|
||||||
}, {
|
|
||||||
label: 'Defer',
|
|
||||||
value: 'defer',
|
|
||||||
}, {
|
|
||||||
label: 'Defer+Madvise',
|
|
||||||
value: 'defer+madvise',
|
|
||||||
}, {
|
|
||||||
label: 'Madvise',
|
|
||||||
value: 'madvise',
|
|
||||||
}, {
|
|
||||||
label: 'Never',
|
|
||||||
value: 'never'
|
|
||||||
}];
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterHugepages',
|
|
||||||
components: { LabeledSelect },
|
|
||||||
|
|
||||||
props: {
|
|
||||||
node: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
registerBeforeHook: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
docsTransparentHugepagesLink() {
|
|
||||||
return DOC.TRANSPARENT_HUGEPAGES;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
async saveHugepages() {
|
|
||||||
this.hugepages['spec'] = this.spec;
|
|
||||||
|
|
||||||
await this.hugepages.save().catch((reason) => {
|
|
||||||
if (reason?.type === 'error') {
|
|
||||||
this.$store.dispatch('growl/error', {
|
|
||||||
title: this.t('harvester.notification.title.error'),
|
|
||||||
message: reason?.message,
|
|
||||||
}, { root: true });
|
|
||||||
|
|
||||||
return Promise.reject(new Error('saveHugepages error'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
|
|
||||||
|
|
||||||
this.hugepages = hash.find((node) => {
|
|
||||||
return node.id === this.node.id;
|
|
||||||
});
|
|
||||||
this.spec = this.hugepages.spec;
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
hugepages: {},
|
|
||||||
spec: { transparent: {} },
|
|
||||||
hugepagesTHPEnabledMode,
|
|
||||||
hugepagesTHPShmemEnabledMode,
|
|
||||||
hugepagesTHPDefragMode,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.registerBeforeHook(this.saveHugepages, 'saveHugepages');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<hr class="divider" />
|
|
||||||
<h3>
|
|
||||||
<t
|
|
||||||
k="harvester.host.hugepages.transparent.title"
|
|
||||||
:raw="true"
|
|
||||||
:url="docsTransparentHugepagesLink"
|
|
||||||
/>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-4">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="spec.transparent.enabled"
|
|
||||||
:label="t('harvester.host.hugepages.transparent.enabled')"
|
|
||||||
:options="hugepagesTHPEnabledMode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-4">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="spec.transparent.shmemEnabled"
|
|
||||||
:label="t('harvester.host.hugepages.transparent.shmemEnabled')"
|
|
||||||
:options="hugepagesTHPShmemEnabledMode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-4">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="spec.transparent.defrag"
|
|
||||||
:label="t('harvester.host.hugepages.transparent.defrag')"
|
|
||||||
:options="hugepagesTHPDefragMode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -7,8 +7,6 @@ import { Checkbox } from '@components/Form/Checkbox';
|
|||||||
import { HCI } from '../../types';
|
import { 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',
|
||||||
@@ -66,13 +64,11 @@ 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,
|
||||||
@@ -103,11 +99,6 @@ 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,7 +28,6 @@ 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';
|
||||||
@@ -51,7 +50,6 @@ export default {
|
|||||||
ArrayListGrouped,
|
ArrayListGrouped,
|
||||||
HarvesterDisk,
|
HarvesterDisk,
|
||||||
HarvesterKsmtuned,
|
HarvesterKsmtuned,
|
||||||
HarvesterHugepages,
|
|
||||||
ButtonDropdown,
|
ButtonDropdown,
|
||||||
KeyValue,
|
KeyValue,
|
||||||
Banner,
|
Banner,
|
||||||
@@ -227,12 +225,6 @@ 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;
|
||||||
|
|
||||||
@@ -535,6 +527,7 @@ 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 {
|
||||||
@@ -655,17 +648,6 @@ 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,13 +10,9 @@ 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, L2VLAN_MODE } from '../config/types';
|
import { NETWORK_TYPE } from '../config/types';
|
||||||
import { removeObject } from '@shell/utils/array';
|
|
||||||
|
|
||||||
const {
|
const { L2VLAN, UNTAGGED, OVERLAY } = NETWORK_TYPE;
|
||||||
L2VLAN, UNTAGGED, OVERLAY, L2TRUNK_VLAN
|
|
||||||
} = NETWORK_TYPE;
|
|
||||||
const { ACCESS, TRUNK } = L2VLAN_MODE;
|
|
||||||
|
|
||||||
const AUTO = 'auto';
|
const AUTO = 'auto';
|
||||||
const MANUAL = 'manual';
|
const MANUAL = 'manual';
|
||||||
@@ -56,8 +52,6 @@ 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 || '',
|
||||||
@@ -114,10 +108,6 @@ 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');
|
||||||
},
|
},
|
||||||
@@ -141,16 +131,6 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
l2VlanModeOptions() {
|
|
||||||
return [{
|
|
||||||
label: this.t('harvester.vlanStatus.vlanConfig.l2VlanMode.access'),
|
|
||||||
value: ACCESS,
|
|
||||||
}, {
|
|
||||||
label: this.t('harvester.vlanStatus.vlanConfig.l2VlanMode.trunk'),
|
|
||||||
value: TRUNK,
|
|
||||||
}];
|
|
||||||
},
|
|
||||||
|
|
||||||
networkTypes() {
|
networkTypes() {
|
||||||
const types = [L2VLAN, UNTAGGED];
|
const types = [L2VLAN, UNTAGGED];
|
||||||
|
|
||||||
@@ -169,22 +149,6 @@ 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;
|
||||||
@@ -204,11 +168,11 @@ export default {
|
|||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
type(newType) {
|
type(newType) {
|
||||||
if (newType === OVERLAY) { // overlay network configuration
|
if (newType === OVERLAY) {
|
||||||
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 { // l2vlan or untagged network configuration
|
} else {
|
||||||
this.config.type = 'bridge';
|
this.config.type = 'bridge';
|
||||||
this.config.promiscMode = true;
|
this.config.promiscMode = true;
|
||||||
this.config.ipam = {};
|
this.config.ipam = {};
|
||||||
@@ -216,20 +180,7 @@ 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: {
|
||||||
@@ -237,15 +188,7 @@ export default {
|
|||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
if (this.isL2VlanNetwork || this.isUntaggedNetwork) {
|
if (this.isL2VlanNetwork || this.isUntaggedNetwork) {
|
||||||
if (this.isL2VlanTrunkMode && this.vlanTrunk.some((trunk) => trunk.minID === '')) {
|
if (!this.config.vlan && !this.isUntaggedNetwork) {
|
||||||
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') }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,30 +217,6 @@ 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 = '';
|
||||||
@@ -373,76 +292,8 @@ 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="isL2VlanAccessMode"
|
v-if="isL2VlanNetwork"
|
||||||
v-model:value.number="config.vlan"
|
v-model:value.number="config.vlan"
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
required
|
required
|
||||||
@@ -465,7 +316,7 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
v-if="isL2VlanAccessMode"
|
v-if="isL2VlanNetwork"
|
||||||
name="layer3Network"
|
name="layer3Network"
|
||||||
:label="t('harvester.network.tabs.layer3Network')"
|
:label="t('harvester.network.tabs.layer3Network')"
|
||||||
:weight="98"
|
:weight="98"
|
||||||
@@ -524,12 +375,3 @@ export default {
|
|||||||
</Tabbed>
|
</Tabbed>
|
||||||
</CruResource>
|
</CruResource>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.remove-btn {
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
.trunk-span{
|
|
||||||
flex: 5;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
@@ -16,8 +15,6 @@ 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';
|
|
||||||
import CronExpressionEditorModal from '@shell/components/Cron/CronExpressionEditorModal.vue';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CreateVMSchedule',
|
name: 'CreateVMSchedule',
|
||||||
@@ -30,7 +27,6 @@ export default {
|
|||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
MessageLink,
|
MessageLink,
|
||||||
Banner,
|
Banner,
|
||||||
CronExpressionEditorModal
|
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
@@ -89,17 +85,15 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { settings: [], showModel: false };
|
return { settings: [] };
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
backupTargetResource() {
|
backupTargetResource() {
|
||||||
return this.settings.find( (O) => O.id === 'backup-target');
|
return this.settings.find( (O) => O.id === 'backup-target');
|
||||||
},
|
},
|
||||||
isEmptyValue() {
|
isEmptyValue() {
|
||||||
return isBackupTargetSettingEmpty(this.backupTargetResource);
|
return this.getBackupTargetValueIsEmpty(this.backupTargetResource);
|
||||||
},
|
},
|
||||||
canUpdate() {
|
canUpdate() {
|
||||||
return this?.backupTargetResource?.canUpdate;
|
return this?.backupTargetResource?.canUpdate;
|
||||||
@@ -115,7 +109,7 @@ export default {
|
|||||||
!!this.value.spec.maxFailure;
|
!!this.value.spec.maxFailure;
|
||||||
},
|
},
|
||||||
isBackupTargetUnAvailable() {
|
isBackupTargetUnAvailable() {
|
||||||
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && isBackupTargetSettingUnavailable(this.backupTargetResource);
|
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && (this.errorMessage || this.isEmptyValue) && this.canUpdate;
|
||||||
},
|
},
|
||||||
vmOptions() {
|
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);
|
||||||
@@ -172,14 +166,25 @@ 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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openModal() {
|
|
||||||
this.showModel = true;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -264,28 +269,16 @@ export default {
|
|||||||
:weight="99"
|
:weight="99"
|
||||||
class="bordered-table"
|
class="bordered-table"
|
||||||
>
|
>
|
||||||
<div class="cronEditor">
|
<LabeledInput
|
||||||
<LabeledInput
|
v-model:value="value.spec.cron"
|
||||||
v-model:value="value.spec.cron"
|
class="mb-30"
|
||||||
class="mb-30"
|
type="cron"
|
||||||
type="cron"
|
required
|
||||||
required
|
:mode="mode"
|
||||||
:mode="mode"
|
:label="t('harvester.schedule.cron')"
|
||||||
:label="t('harvester.schedule.cron.label')"
|
placeholder="0 * * * *"
|
||||||
placeholder="0 * * * *"
|
:disabled="isBackupTargetUnAvailable || isView"
|
||||||
:disabled="isBackupTargetUnAvailable || isView"
|
/>
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="editCronBtn btn role-primary"
|
|
||||||
@click="openModal"
|
|
||||||
>
|
|
||||||
{{ t('harvester.schedule.cron.editButton') }}
|
|
||||||
</button>
|
|
||||||
<CronExpressionEditorModal
|
|
||||||
v-model:show="showModel"
|
|
||||||
v-model:cron-expression="value.spec.cron"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-model:value.number="value.spec.retain"
|
v-model:value.number="value.spec.retain"
|
||||||
class="mb-30"
|
class="mb-30"
|
||||||
@@ -312,16 +305,3 @@ export default {
|
|||||||
</Tabbed>
|
</Tabbed>
|
||||||
</CruResource>
|
</CruResource>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.cronEditor {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editCronBtn {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
margin-left: 10px;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -296,9 +296,4 @@ export default {
|
|||||||
:deep() .edit-help code {
|
:deep() .edit-help code {
|
||||||
padding: 1px 5px;
|
padding: 1px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep(.banner__content.closable) {
|
|
||||||
white-space: normal;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { Checkbox } from '@components/Form/Checkbox';
|
|||||||
import CruResource from '@shell/components/CruResource';
|
import CruResource from '@shell/components/CruResource';
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { set } from '@shell/utils/object';
|
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import KeyValue from '@shell/components/form/KeyValue';
|
import KeyValue from '@shell/components/form/KeyValue';
|
||||||
import NodeScheduling from '@shell/components/form/NodeScheduling';
|
import NodeScheduling from '@shell/components/form/NodeScheduling';
|
||||||
@@ -23,7 +22,6 @@ import Reserved from './kubevirt.io.virtualmachine/VirtualMachineReserved';
|
|||||||
import Volume from './kubevirt.io.virtualmachine/VirtualMachineVolume';
|
import Volume from './kubevirt.io.virtualmachine/VirtualMachineVolume';
|
||||||
import Network from './kubevirt.io.virtualmachine/VirtualMachineNetwork';
|
import Network from './kubevirt.io.virtualmachine/VirtualMachineNetwork';
|
||||||
import CpuMemory from './kubevirt.io.virtualmachine/VirtualMachineCpuMemory';
|
import CpuMemory from './kubevirt.io.virtualmachine/VirtualMachineCpuMemory';
|
||||||
import CpuModel from './kubevirt.io.virtualmachine/VirtualMachineCpuModel';
|
|
||||||
import CloudConfig from './kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
import CloudConfig from './kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
||||||
import SSHKey from './kubevirt.io.virtualmachine/VirtualMachineSSHKey';
|
import SSHKey from './kubevirt.io.virtualmachine/VirtualMachineSSHKey';
|
||||||
|
|
||||||
@@ -40,7 +38,6 @@ export default {
|
|||||||
Network,
|
Network,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
CpuMemory,
|
CpuMemory,
|
||||||
CpuModel,
|
|
||||||
CruResource,
|
CruResource,
|
||||||
CloudConfig,
|
CloudConfig,
|
||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
@@ -73,12 +70,12 @@ export default {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
templateId,
|
templateId,
|
||||||
templateValue: null,
|
templateValue: null,
|
||||||
templateSpec: null,
|
templateSpec: null,
|
||||||
versionName: '',
|
versionName: '',
|
||||||
description: '',
|
description: '',
|
||||||
defaultVersion: null,
|
defaultVersion: null,
|
||||||
isDefaultVersion: false,
|
isDefaultVersion: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -157,18 +154,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
updateCpuModel(value) {
|
|
||||||
if (!this.spec?.template?.spec?.domain?.cpu) {
|
|
||||||
set(this.spec, 'template.spec.domain.cpu', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value && value !== '') {
|
|
||||||
set(this.spec.template.spec.domain.cpu, 'model', value);
|
|
||||||
} else {
|
|
||||||
delete this.spec.template.spec.domain.cpu.model;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async saveVMT(buttonCb) {
|
async saveVMT(buttonCb) {
|
||||||
this.parseVM();
|
this.parseVM();
|
||||||
|
|
||||||
@@ -451,17 +436,6 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<CpuModel
|
|
||||||
:value="spec.template.spec.domain.cpu?.model || ''"
|
|
||||||
:mode="mode"
|
|
||||||
@update:value="updateCpuModel"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-20">
|
<div class="row mb-20">
|
||||||
<a
|
<a
|
||||||
v-if="showAdvanced"
|
v-if="showAdvanced"
|
||||||
|
|||||||
@@ -7,13 +7,9 @@ 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 { Checkbox } from '@components/Form/Checkbox';
|
|
||||||
import jsyaml from 'js-yaml';
|
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { clone, get } from '@shell/utils/object';
|
import { get } from '@shell/utils/object';
|
||||||
import { STORAGE_CLASS, LONGHORN, PV } from '@shell/config/types';
|
import { STORAGE_CLASS, LONGHORN, PV } from '@shell/config/types';
|
||||||
import { sortBy } from '@shell/utils/sort';
|
import { sortBy } from '@shell/utils/sort';
|
||||||
import { saferDump } from '@shell/utils/create-yaml';
|
import { saferDump } from '@shell/utils/create-yaml';
|
||||||
@@ -36,7 +32,6 @@ export default {
|
|||||||
|
|
||||||
components: {
|
components: {
|
||||||
Banner,
|
Banner,
|
||||||
Checkbox,
|
|
||||||
Tab,
|
Tab,
|
||||||
UnitInput,
|
UnitInput,
|
||||||
CruResource,
|
CruResource,
|
||||||
@@ -45,7 +40,6 @@ export default {
|
|||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
LabeledInput,
|
LabeledInput,
|
||||||
NameNsDescription,
|
NameNsDescription,
|
||||||
Conditions
|
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
@@ -94,34 +88,14 @@ export default {
|
|||||||
source,
|
source,
|
||||||
storage,
|
storage,
|
||||||
imageId,
|
imageId,
|
||||||
showAdvanced: false,
|
snapshots: [],
|
||||||
createWithDataVolume: false,
|
images: [],
|
||||||
snapshots: [],
|
|
||||||
images: [],
|
|
||||||
GIBIBYTE
|
GIBIBYTE
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.registerBeforeHook(this.willSave, 'willSave');
|
this.registerBeforeHook(this.willSave, 'willSave');
|
||||||
|
|
||||||
if (this.mode === _CREATE) {
|
|
||||||
const originalSaveYaml = this.value.saveYaml?.bind(this.value);
|
|
||||||
|
|
||||||
this.value.saveYaml = async(yaml) => {
|
|
||||||
if (this.createWithDataVolume && this.isBlank) {
|
|
||||||
const parsed = jsyaml.load(yaml);
|
|
||||||
const dvObj = { ...parsed, type: 'cdi.kubevirt.io.datavolume' };
|
|
||||||
const dataVolume = await this.$store.dispatch('harvester/create', dvObj);
|
|
||||||
|
|
||||||
await dataVolume.save();
|
|
||||||
|
|
||||||
return dataVolume;
|
|
||||||
}
|
|
||||||
|
|
||||||
return originalSaveYaml(yaml);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
@@ -159,10 +133,6 @@ export default {
|
|||||||
return Object.values(VOLUME_MODE);
|
return Object.values(VOLUME_MODE);
|
||||||
},
|
},
|
||||||
|
|
||||||
accessModeOptions() {
|
|
||||||
return ['ReadWriteOnce', 'ReadWriteMany', 'ReadOnlyMany'];
|
|
||||||
},
|
|
||||||
|
|
||||||
imageOption() {
|
imageOption() {
|
||||||
return sortBy(
|
return sortBy(
|
||||||
this.images
|
this.images
|
||||||
@@ -297,23 +267,7 @@ 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');
|
|
||||||
},
|
|
||||||
|
|
||||||
isCreatePVCWithDataVolumeFeatureEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('createPVCWithDataVolume');
|
|
||||||
},
|
|
||||||
|
|
||||||
isResizeDisabled() {
|
|
||||||
return (
|
|
||||||
!this.isLHV2VolExpansionFeatureEnabled &&
|
|
||||||
this.value?.isLonghornV2 &&
|
|
||||||
this.isEdit
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@@ -373,58 +327,6 @@ export default {
|
|||||||
|
|
||||||
return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany'];
|
return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany'];
|
||||||
},
|
},
|
||||||
buildDataVolumeObj() {
|
|
||||||
const storage = {
|
|
||||||
storageClassName: this.value.spec.storageClassName,
|
|
||||||
resources: { requests: { storage: this.storage } },
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.showAdvanced && this.value.spec.accessModes?.length > 0) {
|
|
||||||
storage.accessModes = this.value.spec.accessModes;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.showAdvanced && this.value.spec.volumeMode) {
|
|
||||||
storage.volumeMode = this.value.spec.volumeMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'cdi.kubevirt.io.datavolume',
|
|
||||||
apiVersion: 'cdi.kubevirt.io/v1beta1',
|
|
||||||
kind: 'DataVolume',
|
|
||||||
metadata: {
|
|
||||||
name: this.value.metadata.name,
|
|
||||||
namespace: this.value.metadata.namespace,
|
|
||||||
annotations: this.value.metadata.annotations || {},
|
|
||||||
labels: this.value.metadata.labels || {},
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
source: { blank: {} },
|
|
||||||
storage,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
async save(buttonDone) {
|
|
||||||
if (this.isCreate && this.isBlank && this.createWithDataVolume) {
|
|
||||||
try {
|
|
||||||
this.update();
|
|
||||||
const dvObj = this.buildDataVolumeObj();
|
|
||||||
const dataVolume = await this.$store.dispatch('harvester/create', dvObj);
|
|
||||||
|
|
||||||
await dataVolume.save();
|
|
||||||
buttonDone(true);
|
|
||||||
this.done();
|
|
||||||
} catch (err) {
|
|
||||||
const error = err?.data || err;
|
|
||||||
|
|
||||||
this['errors'] = exceptionToErrorsArray(error);
|
|
||||||
buttonDone(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await CreateEditView.methods.save.call(this, buttonDone);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
willSave() {
|
willSave() {
|
||||||
this.update();
|
this.update();
|
||||||
},
|
},
|
||||||
@@ -467,17 +369,9 @@ export default {
|
|||||||
this.update();
|
this.update();
|
||||||
},
|
},
|
||||||
generateYaml() {
|
generateYaml() {
|
||||||
this.update();
|
const out = saferDump(this.value);
|
||||||
|
|
||||||
if (this.isCreate && this.isBlank && this.createWithDataVolume) {
|
return out;
|
||||||
return saferDump(this.buildDataVolumeObj());
|
|
||||||
}
|
|
||||||
|
|
||||||
const plain = clone(this.value);
|
|
||||||
|
|
||||||
delete plain.saveYaml;
|
|
||||||
|
|
||||||
return saferDump(plain);
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -550,6 +444,18 @@ export default {
|
|||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-if="showVolumeMode"
|
||||||
|
v-model:value="value.spec.volumeMode"
|
||||||
|
:label="t('harvester.volume.volumeMode')"
|
||||||
|
:options="volumeModeOptions"
|
||||||
|
required
|
||||||
|
:disabled="!isCreate"
|
||||||
|
:mode="mode"
|
||||||
|
class="mb-20"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
|
||||||
<UnitInput
|
<UnitInput
|
||||||
v-model:value="storage"
|
v-model:value="storage"
|
||||||
:label="t('harvester.volume.size')"
|
:label="t('harvester.volume.size')"
|
||||||
@@ -557,7 +463,7 @@ export default {
|
|||||||
:output-modifier="true"
|
:output-modifier="true"
|
||||||
:increment="1024"
|
:increment="1024"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:disabled="isResizeDisabled"
|
:disabled="value?.isLonghornV2 && isEdit"
|
||||||
required
|
required
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
:suffix="GIBIBYTE"
|
:suffix="GIBIBYTE"
|
||||||
@@ -565,49 +471,11 @@ export default {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Banner
|
<Banner
|
||||||
v-if="isResizeDisabled"
|
v-if="value?.isLonghornV2 && isEdit"
|
||||||
color="warning"
|
color="warning"
|
||||||
>
|
>
|
||||||
<span>{{ t('harvester.volume.longhorn.disableResize') }}</span>
|
<span>{{ t('harvester.volume.longhorn.disableResize') }}</span>
|
||||||
</Banner>
|
</Banner>
|
||||||
|
|
||||||
<div class="row mb-20">
|
|
||||||
<Checkbox
|
|
||||||
v-if="isCreate && isBlank && isCreatePVCWithDataVolumeFeatureEnabled"
|
|
||||||
v-model:value="createWithDataVolume"
|
|
||||||
:label="t('harvester.volume.createWithDataVolume')"
|
|
||||||
tooltip-key="harvester.volume.createWithDataVolumeTooltip"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
v-if="isCreate && isCreatePVCWithDataVolumeFeatureEnabled"
|
|
||||||
role="button"
|
|
||||||
class="hand"
|
|
||||||
@click="showAdvanced = !showAdvanced"
|
|
||||||
>
|
|
||||||
{{ showAdvanced ? t('harvester.volume.hideAdvanced') : t('harvester.volume.showAdvanced') }}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<LabeledSelect
|
|
||||||
v-if="showAdvanced"
|
|
||||||
v-model:value="value.spec.accessModes"
|
|
||||||
:label="t('harvester.volume.accessModes')"
|
|
||||||
:options="accessModeOptions"
|
|
||||||
:multiple="true"
|
|
||||||
:mode="mode"
|
|
||||||
class="mb-20 mt-20"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledSelect
|
|
||||||
v-if="showAdvanced"
|
|
||||||
v-model:value="value.spec.volumeMode"
|
|
||||||
:label="t('harvester.volume.volumeMode')"
|
|
||||||
:options="volumeModeOptions"
|
|
||||||
:mode="mode"
|
|
||||||
class="mb-20"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
v-if="!isCreate"
|
v-if="!isCreate"
|
||||||
@@ -713,15 +581,6 @@ 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>
|
||||||
|
|||||||
@@ -1,196 +0,0 @@
|
|||||||
<script>
|
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
|
||||||
import { removeAt } from '@shell/utils/array';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import InfoBox from '@shell/components/InfoBox';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'AccessControlList',
|
|
||||||
|
|
||||||
emits: ['update:value'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
Banner, InfoBox, LabeledSelect, LabeledInput
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Array,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
default: _EDIT,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
const rows = (this.value || []).map((row) => {
|
|
||||||
return {
|
|
||||||
action: row.action || '',
|
|
||||||
direction: row.direction || '',
|
|
||||||
priority: row.priority || 0,
|
|
||||||
match: row.match || '',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return { rows };
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
isView() {
|
|
||||||
return this.mode === _VIEW;
|
|
||||||
},
|
|
||||||
|
|
||||||
actionOptions() {
|
|
||||||
return [
|
|
||||||
{ label: 'Allow', value: 'allow' },
|
|
||||||
{ label: 'Drop', value: 'drop' },
|
|
||||||
{ label: 'Pass', value: 'pass' },
|
|
||||||
{ label: 'Reject', value: 'reject' },
|
|
||||||
{ label: 'Allow-related', value: 'allow-related' },
|
|
||||||
{ label: 'Allow-stateless', value: 'allow-stateless' },
|
|
||||||
];
|
|
||||||
},
|
|
||||||
directionOptions() {
|
|
||||||
return [
|
|
||||||
{ label: 'To-lport', value: 'to-lport' },
|
|
||||||
{ label: 'From-lport', value: 'from-lport' },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.queueUpdate = debounce(this.update, 100);
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
add() {
|
|
||||||
this.rows.push({
|
|
||||||
action: '',
|
|
||||||
direction: '',
|
|
||||||
priority: this.rows.length,
|
|
||||||
match: '',
|
|
||||||
});
|
|
||||||
this.queueUpdate();
|
|
||||||
},
|
|
||||||
|
|
||||||
removeRule(idx) {
|
|
||||||
removeAt(this.rows, idx);
|
|
||||||
this.queueUpdate();
|
|
||||||
},
|
|
||||||
|
|
||||||
update() {
|
|
||||||
if (this.isView) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$emit('update:value', this.rows);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<Banner
|
|
||||||
v-if="rows.length > 0"
|
|
||||||
color="info"
|
|
||||||
>
|
|
||||||
<t
|
|
||||||
k="harvester.subnet.acl.banner"
|
|
||||||
:raw="true"
|
|
||||||
/>
|
|
||||||
</Banner>
|
|
||||||
<div
|
|
||||||
v-for="(row, idx) in rows"
|
|
||||||
:key="idx"
|
|
||||||
>
|
|
||||||
<InfoBox class="box">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="role-link btn btn-sm removeBtn"
|
|
||||||
:disabled="isView"
|
|
||||||
@click="removeRule(idx)"
|
|
||||||
>
|
|
||||||
<i class="icon icon-x" />
|
|
||||||
</button>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-4">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="row.action"
|
|
||||||
:mode="mode"
|
|
||||||
class="mt-5 ml-5"
|
|
||||||
:options="actionOptions"
|
|
||||||
:required="true"
|
|
||||||
:label="t('harvester.subnet.acl.action.label')"
|
|
||||||
:placeholder="t('harvester.subnet.acl.action.placeholder')"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-4">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="row.direction"
|
|
||||||
:mode="mode"
|
|
||||||
:options="directionOptions"
|
|
||||||
class="mt-5"
|
|
||||||
:required="true"
|
|
||||||
:label="t('harvester.subnet.acl.direction.label')"
|
|
||||||
:placeholder="t('harvester.subnet.acl.direction.placeholder')"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-3">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value.number="row.priority"
|
|
||||||
type="number"
|
|
||||||
class="mb-20 mt-5"
|
|
||||||
:max="32767"
|
|
||||||
:min="0"
|
|
||||||
:mode="mode"
|
|
||||||
required
|
|
||||||
label-key="harvester.subnet.acl.priority.label"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-11">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="row.match"
|
|
||||||
class="mb-5 ml-5"
|
|
||||||
:mode="mode"
|
|
||||||
required
|
|
||||||
:placeholder="t('harvester.subnet.acl.match.placeholder')"
|
|
||||||
label-key="harvester.subnet.acl.match.label"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</InfoBox>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn role-tertiary add"
|
|
||||||
:disabled="isView"
|
|
||||||
@click="add()"
|
|
||||||
>
|
|
||||||
<t k="harvester.subnet.acl.addRule" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.box {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.removeBtn {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -14,8 +14,6 @@ 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',
|
||||||
@@ -23,7 +21,6 @@ export default {
|
|||||||
emits: ['update:value'],
|
emits: ['update:value'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
Banner,
|
|
||||||
CruResource,
|
CruResource,
|
||||||
LabeledInput,
|
LabeledInput,
|
||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
@@ -33,7 +30,6 @@ export default {
|
|||||||
ArrayList,
|
ArrayList,
|
||||||
ResourceTabs,
|
ResourceTabs,
|
||||||
Loading,
|
Loading,
|
||||||
AccessControlList
|
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
@@ -42,9 +38,7 @@ 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,
|
||||||
@@ -52,9 +46,7 @@ export default {
|
|||||||
vpc,
|
vpc,
|
||||||
gatewayIP: '',
|
gatewayIP: '',
|
||||||
excludeIps: [],
|
excludeIps: [],
|
||||||
private: false,
|
private: false
|
||||||
enableDHCP,
|
|
||||||
acls: []
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -89,6 +81,7 @@ 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;
|
||||||
@@ -132,21 +125,10 @@ 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) {
|
||||||
@@ -157,8 +139,6 @@ 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) {
|
||||||
@@ -265,45 +245,6 @@ 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
|
||||||
@@ -371,29 +312,12 @@ export default {
|
|||||||
<div class="key">
|
<div class="key">
|
||||||
<input
|
<input
|
||||||
v-model="scope.row.value"
|
v-model="scope.row.value"
|
||||||
:placeholder="t('harvester.subnet.excludeIPs.placeholder')"
|
:placeholder="t('harvester.setting.storageNetwork.exclude.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,17 +2,12 @@
|
|||||||
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,
|
||||||
@@ -50,12 +45,6 @@ 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() {
|
||||||
@@ -89,13 +78,6 @@ 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"
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
<script>
|
|
||||||
import YAML from 'yaml';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import { CONFIG_MAP } from '@shell/config/types';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
|
|
||||||
const CPU_MODEL_CONFIG_MAP_ID = 'harvester-system/node-cpu-model-configuration';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterCpuModel',
|
|
||||||
|
|
||||||
emits: ['update:value'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
LabeledSelect,
|
|
||||||
Banner
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
default: 'create',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.$store.dispatch(`${ inStore }/find`, { type: CONFIG_MAP, id: CPU_MODEL_CONFIG_MAP_ID });
|
|
||||||
this.fetchError = null;
|
|
||||||
} catch (e) {
|
|
||||||
this.fetchError = this.t('harvester.virtualMachine.cpuModel.fetchError', { error: e.message || e });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return { fetchError: null };
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
localValue: {
|
|
||||||
get() {
|
|
||||||
return this.value ?? '';
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$emit('update:value', val ?? '');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
cpuModelConfigMap() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
return this.$store.getters[`${ inStore }/byId`](
|
|
||||||
CONFIG_MAP,
|
|
||||||
CPU_MODEL_CONFIG_MAP_ID
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
cpuModelOptions() {
|
|
||||||
if (!this.cpuModelConfigMap?.data?.cpuModels) {
|
|
||||||
return [{ label: this.t('generic.default'), value: '' }];
|
|
||||||
}
|
|
||||||
|
|
||||||
let cpuModelsData;
|
|
||||||
|
|
||||||
try {
|
|
||||||
cpuModelsData = YAML.parse(this.cpuModelConfigMap.data?.cpuModels || '');
|
|
||||||
} catch (e) {
|
|
||||||
return [{ label: this.t('generic.default'), value: '' }];
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = [];
|
|
||||||
|
|
||||||
options.push({
|
|
||||||
label: this.t('generic.default'),
|
|
||||||
value: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add global models (host-model, host-passthrough)
|
|
||||||
const globalModels = cpuModelsData.globalModels || [];
|
|
||||||
|
|
||||||
globalModels.forEach((modelName) => {
|
|
||||||
options.push({
|
|
||||||
label: modelName,
|
|
||||||
value: modelName
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add regular models with node count
|
|
||||||
const modelEntries = Object.entries(cpuModelsData.models || {});
|
|
||||||
|
|
||||||
// Sort models alphabetically for consistent display
|
|
||||||
modelEntries.sort((a, b) => a[0].localeCompare(b[0]));
|
|
||||||
|
|
||||||
modelEntries.forEach(([modelName, modelInfo]) => {
|
|
||||||
const readyCount = modelInfo.readyCount || 0;
|
|
||||||
const label = this.t('harvester.virtualMachine.cpuModel.optionLabel', { modelName, count: readyCount });
|
|
||||||
|
|
||||||
options.push({
|
|
||||||
label,
|
|
||||||
value: modelName
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return options;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<Banner
|
|
||||||
v-if="fetchError"
|
|
||||||
color="error"
|
|
||||||
class="mb-20"
|
|
||||||
>
|
|
||||||
{{ fetchError }}
|
|
||||||
</Banner>
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="localValue"
|
|
||||||
:label="t('harvester.virtualMachine.cpuModel.label')"
|
|
||||||
:options="cpuModelOptions"
|
|
||||||
:mode="mode"
|
|
||||||
:disabled="!!fetchError"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script>
|
<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';
|
||||||
@@ -13,13 +14,6 @@ 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'
|
||||||
@@ -38,15 +32,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
await this.fetchHotunplugData();
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
nameIdx: 1,
|
rows: this.addKeyId(clone(this.value)),
|
||||||
rows: this.addKeyId(clone(this.value)),
|
nameIdx: 1
|
||||||
hotunpluggableNics: new Set(),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -75,57 +64,15 @@ 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 = this.mergeHotplugData(clone(neu));
|
this.rows = 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();
|
||||||
|
|
||||||
@@ -171,11 +118,7 @@ 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>
|
||||||
@@ -186,28 +129,17 @@ export default {
|
|||||||
v-for="(row, i) in rows"
|
v-for="(row, i) in rows"
|
||||||
:key="i"
|
:key="i"
|
||||||
>
|
>
|
||||||
<div class="box-title mb-10">
|
<button
|
||||||
<h3>
|
v-if="!isView"
|
||||||
{{ t('harvester.virtualMachine.network.title') }}
|
type="button"
|
||||||
</h3>
|
class="role-link remove-vol"
|
||||||
<button
|
@click="remove(row)"
|
||||||
v-if="!isView"
|
>
|
||||||
type="button"
|
<i class="icon icon-x" />
|
||||||
class="role-link btn btn-sm remove"
|
</button>
|
||||||
@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"
|
||||||
@@ -230,13 +162,16 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang='scss' scoped>
|
<style lang='scss' scoped>
|
||||||
.box-title{
|
.infoBox{
|
||||||
display: flex;
|
position: relative;
|
||||||
justify-content: space-between;
|
}
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
h3 {
|
.remove-vol {
|
||||||
margin-bottom: 0;
|
position: absolute;
|
||||||
}
|
top: 10px;
|
||||||
|
right: 16px;
|
||||||
|
padding:0px;
|
||||||
|
max-height: 28px;
|
||||||
|
min-height: 28px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+8
-31
@@ -31,7 +31,6 @@ export default {
|
|||||||
const _hash = {
|
const _hash = {
|
||||||
pciclaims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.PCI_CLAIM }),
|
pciclaims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.PCI_CLAIM }),
|
||||||
sriovs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOV }),
|
sriovs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOV }),
|
||||||
srigpuovs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOVGPU_DEVICE }),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await allHash(_hash);
|
await allHash(_hash);
|
||||||
@@ -107,32 +106,19 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
allSriovs() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
return this.$store.getters[`${ inStore }/all`](HCI.SR_IOV) || [];
|
|
||||||
},
|
|
||||||
allSriovGPUs() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
return this.$store.getters[`${ inStore }/all`](HCI.SR_IOVGPU_DEVICE) || [];
|
|
||||||
},
|
|
||||||
parentSriovOptions() {
|
parentSriovOptions() {
|
||||||
return this.allSriovs.map((sriov) => sriov.id);
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
},
|
const allSriovs = this.$store.getters[`${ inStore }/all`](HCI.SR_IOV) || [];
|
||||||
parentSriovGPUOptions() {
|
|
||||||
return this.allSriovGPUs.map((sriovgpu) => sriovgpu.id);
|
return allSriovs.map((sriov) => {
|
||||||
|
return sriov.id;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
parentSriovLabel() {
|
parentSriovLabel() {
|
||||||
return HCI_ANNOTATIONS.PARENT_SRIOV;
|
return HCI_ANNOTATIONS.PARENT_SRIOV;
|
||||||
},
|
}
|
||||||
parentSriovGPULabel() {
|
|
||||||
return HCI_ANNOTATIONS.PARENT_SRIOV_GPU;
|
|
||||||
},
|
|
||||||
vGPUAsPCIDeviceEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
enableGroup(rows = []) {
|
enableGroup(rows = []) {
|
||||||
const row = rows[0];
|
const row = rows[0];
|
||||||
@@ -220,15 +206,6 @@ export default {
|
|||||||
:rows="rows"
|
:rows="rows"
|
||||||
@change-rows="changeRows"
|
@change-rows="changeRows"
|
||||||
/>
|
/>
|
||||||
<FilterBySriov
|
|
||||||
v-if="vGPUAsPCIDeviceEnabled"
|
|
||||||
ref="filterByParentSRIOVGPU"
|
|
||||||
:parent-sriov-options="parentSriovGPUOptions"
|
|
||||||
:parent-sriov-label="parentSriovGPULabel"
|
|
||||||
:label="t('harvester.sriov.parentSriovGPU')"
|
|
||||||
:rows="rows"
|
|
||||||
@change-rows="changeRows"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</ResourceTable>
|
</ResourceTable>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { set } from '@shell/utils/object';
|
|||||||
import { HCI } from '../../../types';
|
import { HCI } from '../../../types';
|
||||||
import DeviceList from './DeviceList';
|
import DeviceList from './DeviceList';
|
||||||
import CompatibilityMatrix from '../CompatibilityMatrix';
|
import CompatibilityMatrix from '../CompatibilityMatrix';
|
||||||
import MessageLink from '@shell/components/MessageLink';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'VirtualMachinePCIDevices',
|
name: 'VirtualMachinePCIDevices',
|
||||||
@@ -16,8 +15,7 @@ export default {
|
|||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
DeviceList,
|
DeviceList,
|
||||||
CompatibilityMatrix,
|
CompatibilityMatrix,
|
||||||
Banner,
|
Banner
|
||||||
MessageLink
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
mode: {
|
mode: {
|
||||||
@@ -56,11 +54,6 @@ export default {
|
|||||||
|
|
||||||
const vmDevices = this.value?.domain?.devices?.hostDevices || [];
|
const vmDevices = this.value?.domain?.devices?.hostDevices || [];
|
||||||
const otherDevices = this.otherDevices(vmDevices).map(({ name }) => name);
|
const otherDevices = this.otherDevices(vmDevices).map(({ name }) => name);
|
||||||
const vmDeviceNames = vmDevices.map(({ name }) => name);
|
|
||||||
|
|
||||||
this.pciDevices.forEach((row) => {
|
|
||||||
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
vmDevices.forEach(({ name, deviceName }) => {
|
vmDevices.forEach(({ name, deviceName }) => {
|
||||||
const checkName = (deviceName || '').split('/')?.[1];
|
const checkName = (deviceName || '').split('/')?.[1];
|
||||||
@@ -140,13 +133,6 @@ export default {
|
|||||||
return inUse;
|
return inUse;
|
||||||
},
|
},
|
||||||
|
|
||||||
toVGpuDevicesPage() {
|
|
||||||
return {
|
|
||||||
name: 'harvester-c-cluster-resource',
|
|
||||||
params: { cluster: this.$store.getters['clusterId'], resource: HCI.VGPU_DEVICE },
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
devicesByNode() {
|
devicesByNode() {
|
||||||
return this.enabledDevices?.reduce((acc, device) => {
|
return this.enabledDevices?.reduce((acc, device) => {
|
||||||
const nodeName = device.status?.nodeName;
|
const nodeName = device.status?.nodeName;
|
||||||
@@ -241,12 +227,7 @@ export default {
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col span-12">
|
<div class="col span-12">
|
||||||
<Banner color="info">
|
<Banner color="info">
|
||||||
<MessageLink
|
<t k="harvester.pci.howToUseDevice" />
|
||||||
:to="toVGpuDevicesPage"
|
|
||||||
prefix-label="harvester.pci.howToUseDeviceInVMCreation.prefix"
|
|
||||||
middle-label="harvester.pci.howToUseDeviceInVMCreation.middle"
|
|
||||||
suffix-label="harvester.pci.howToUseDeviceInVMCreation.suffix"
|
|
||||||
/>
|
|
||||||
</Banner>
|
</Banner>
|
||||||
<Banner
|
<Banner
|
||||||
v-if="selectedDevices.length > 0"
|
v-if="selectedDevices.length > 0"
|
||||||
|
|||||||
@@ -48,13 +48,6 @@ export default {
|
|||||||
this[key] = res[key];
|
this[key] = res[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
const vmDevices = this.value?.domain?.devices?.hostDevices || [];
|
|
||||||
const vmDeviceNames = vmDevices.map(({ name }) => name);
|
|
||||||
|
|
||||||
this.devices.forEach((row) => {
|
|
||||||
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.selectedDevices = (this.value?.domain?.devices?.hostDevices || [])
|
this.selectedDevices = (this.value?.domain?.devices?.hostDevices || [])
|
||||||
.map(({ name }) => name)
|
.map(({ name }) => name)
|
||||||
.filter((name) => this.enabledDevices.find((device) => device?.metadata?.name === name));
|
.filter((name) => this.enabledDevices.find((device) => device?.metadata?.name === name));
|
||||||
|
|||||||
@@ -46,13 +46,6 @@ export default {
|
|||||||
this[key] = res[key];
|
this[key] = res[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
const vmDevices = this.value?.domain?.devices?.gpus || [];
|
|
||||||
const vmDeviceNames = vmDevices.map(({ name }) => name);
|
|
||||||
|
|
||||||
this.devices.forEach((row) => {
|
|
||||||
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
const vGpus = this.vm.isOff ? [
|
const vGpus = this.vm.isOff ? [
|
||||||
...(this.value?.domain?.devices?.gpus || []).map(({ name }) => name),
|
...(this.value?.domain?.devices?.gpus || []).map(({ name }) => name),
|
||||||
] : [
|
] : [
|
||||||
|
|||||||
@@ -13,12 +13,11 @@ import { ucFirst, randomStr } from '@shell/utils/string';
|
|||||||
import { removeObject } from '@shell/utils/array';
|
import { removeObject } from '@shell/utils/array';
|
||||||
import { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
|
import { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
|
||||||
import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
|
import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
|
||||||
import { VOLUME_HOTPLUG_ACTION, SOURCE_TYPE } from '../../../config/harvester-map';
|
import { SOURCE_TYPE } from '../../../config/harvester-map';
|
||||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
import { 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';
|
import { OFF } from '../../../models/kubevirt.io.virtualmachine';
|
||||||
import { EMPTY_IMAGE } from '../../../utils/vm';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ['update:value'],
|
emits: ['update:value'],
|
||||||
@@ -118,10 +117,6 @@ export default {
|
|||||||
return this.mode === _CREATE;
|
return this.mode === _CREATE;
|
||||||
},
|
},
|
||||||
|
|
||||||
isHotplugCdRomFeatureEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('hotplugCdRom');
|
|
||||||
},
|
|
||||||
|
|
||||||
defaultStorageClass() {
|
defaultStorageClass() {
|
||||||
const defaultStorage = this.$store.getters['harvester/all'](STORAGE_CLASS).find((sc) => sc.isDefault);
|
const defaultStorage = this.$store.getters['harvester/all'](STORAGE_CLASS).find((sc) => sc.isDefault);
|
||||||
|
|
||||||
@@ -141,17 +136,13 @@ 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: {
|
||||||
value: {
|
value: {
|
||||||
handler(neu) {
|
handler(neu) {
|
||||||
const rows = clone(neu).map((V) => {
|
const rows = clone(neu).map((V) => {
|
||||||
if (!this.isCreate && V.source !== SOURCE_TYPE.CONTAINER && !V.newCreateId && V.image !== EMPTY_IMAGE) {
|
if (!this.isCreate && V.source !== SOURCE_TYPE.CONTAINER && !V.newCreateId) {
|
||||||
V.to = {
|
V.to = {
|
||||||
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-namespace-id`,
|
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-namespace-id`,
|
||||||
params: {
|
params: {
|
||||||
@@ -222,48 +213,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
canDoVolumeHotplugAction(volume) {
|
unplugVolume(volume) {
|
||||||
if (!this.isHotplugCdRomFeatureEnabled && volume.type === 'cd-rom') {
|
this.vm.unplugVolume(volume.name);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (volume.hotpluggable) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return volume.type === 'cd-rom' && volume.bus === 'sata' && volume.image === EMPTY_IMAGE;
|
|
||||||
},
|
|
||||||
|
|
||||||
getVolumeHotplugAction(volume) {
|
|
||||||
if (volume.type === 'cd-rom' && volume.bus === 'sata') {
|
|
||||||
if (volume.image === EMPTY_IMAGE) {
|
|
||||||
return VOLUME_HOTPLUG_ACTION.INSERT_CDROM_IMAGE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return VOLUME_HOTPLUG_ACTION.EJECT_CDROM_IMAGE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return VOLUME_HOTPLUG_ACTION.DETACH_DISK;
|
|
||||||
},
|
|
||||||
|
|
||||||
getVolumeHotplugActionLabel(volume) {
|
|
||||||
const labels = {
|
|
||||||
[VOLUME_HOTPLUG_ACTION.DETACH_DISK]: 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabel',
|
|
||||||
[VOLUME_HOTPLUG_ACTION.INSERT_CDROM_IMAGE]: 'harvester.modal.insertCdRomVolume.actionLabel',
|
|
||||||
[VOLUME_HOTPLUG_ACTION.EJECT_CDROM_IMAGE]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.actionLabel',
|
|
||||||
};
|
|
||||||
|
|
||||||
return labels[this.getVolumeHotplugAction(volume)];
|
|
||||||
},
|
|
||||||
|
|
||||||
hotplugVolume(volume) {
|
|
||||||
const calls = {
|
|
||||||
[VOLUME_HOTPLUG_ACTION.DETACH_DISK]: () => this.vm.unplugVolume(volume.name),
|
|
||||||
[VOLUME_HOTPLUG_ACTION.INSERT_CDROM_IMAGE]: () => this.vm.insertCdRomVolume(volume.name),
|
|
||||||
[VOLUME_HOTPLUG_ACTION.EJECT_CDROM_IMAGE]: () => this.vm.ejectCdRomVolume(volume.name),
|
|
||||||
};
|
|
||||||
|
|
||||||
return calls[this.getVolumeHotplugAction(volume)]();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentFor(type) {
|
componentFor(type) {
|
||||||
@@ -319,8 +270,8 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
isResizeDisabled(volume) {
|
isResizeDisabled(volume) {
|
||||||
if (this.isLHV2VolExpansionFeatureEnabled) return false;
|
if (this.isCreate) return false;
|
||||||
if (this.isCreate || volume.newCreateId) return false;
|
if (volume.newCreateId) return false;
|
||||||
|
|
||||||
const isStopped = this.vm.stateDisplay === OFF;
|
const isStopped = this.vm.stateDisplay === OFF;
|
||||||
const isLonghornV2 = this.isLonghornV2(volume);
|
const isLonghornV2 = this.isLonghornV2(volume);
|
||||||
@@ -348,57 +299,55 @@ export default {
|
|||||||
v-for="(volume, i) in rows"
|
v-for="(volume, i) in rows"
|
||||||
:key="volume.id"
|
:key="volume.id"
|
||||||
>
|
>
|
||||||
<InfoBox>
|
<InfoBox class="box">
|
||||||
<div class="box-title mb-10">
|
<button
|
||||||
<h3>
|
v-if="!isView"
|
||||||
<span
|
type="button"
|
||||||
v-if="volume.to && isVirtualType"
|
class="role-link btn btn-sm remove"
|
||||||
class="title"
|
@click="removeVolume(volume)"
|
||||||
|
>
|
||||||
|
<i class="icon icon-x" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="volume.hotpluggable && isView"
|
||||||
|
type="button"
|
||||||
|
class="role-link btn remove"
|
||||||
|
@click="unplugVolume(volume)"
|
||||||
|
>
|
||||||
|
{{ t('harvester.virtualMachine.unplug.detachVolume') }}
|
||||||
|
</button>
|
||||||
|
<h3>
|
||||||
|
<span
|
||||||
|
v-if="volume.to && isVirtualType"
|
||||||
|
class="title"
|
||||||
|
>
|
||||||
|
<router-link :to="volume.to">
|
||||||
|
{{ t('harvester.virtualMachine.volume.edit') }} {{ headerFor(volume.source) }}
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<BadgeStateFormatter
|
||||||
|
v-if="volume.pvc"
|
||||||
|
class="ml-10 state"
|
||||||
|
:arbitrary="true"
|
||||||
|
:row="volume.pvc"
|
||||||
|
:value="volume.pvc.state"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
v-if="dev && !!volume.pvc && !!volume.pvc.resourceExternalLink"
|
||||||
|
v-clean-tooltip="t(volume.pvc.resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
|
||||||
|
class="ml-5 resource-external"
|
||||||
|
rel="nofollow noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
:href="volume.pvc.resourceExternalLink.url"
|
||||||
>
|
>
|
||||||
<router-link :to="volume.to">
|
<i class="icon icon-external-link" />
|
||||||
{{ t('harvester.virtualMachine.volume.edit') }} {{ headerFor(volume.source) }}
|
</a>
|
||||||
</router-link>
|
</span>
|
||||||
|
|
||||||
<BadgeStateFormatter
|
<span v-else>
|
||||||
v-if="volume.pvc"
|
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
|
||||||
class="ml-10 state"
|
</span>
|
||||||
:arbitrary="true"
|
</h3>
|
||||||
:row="volume.pvc"
|
|
||||||
:value="volume.pvc.state"
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
v-if="dev && !!volume.pvc && !!volume.pvc.resourceExternalLink"
|
|
||||||
v-clean-tooltip="t(volume.pvc.resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
|
|
||||||
class="ml-5 resource-external"
|
|
||||||
rel="nofollow noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
:href="volume.pvc.resourceExternalLink.url"
|
|
||||||
>
|
|
||||||
<i class="icon icon-external-link" />
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span v-else>
|
|
||||||
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
|
|
||||||
</span>
|
|
||||||
</h3>
|
|
||||||
<button
|
|
||||||
v-if="!isView"
|
|
||||||
type="button"
|
|
||||||
class="role-link btn btn-sm remove"
|
|
||||||
@click="removeVolume(volume)"
|
|
||||||
>
|
|
||||||
<i class="icon icon-x" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="canDoVolumeHotplugAction(volume) && isView"
|
|
||||||
type="button"
|
|
||||||
class="role-link btn btn-sm remove"
|
|
||||||
@click="hotplugVolume(volume)"
|
|
||||||
>
|
|
||||||
{{ t(getVolumeHotplugActionLabel(volume)) }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<component
|
<component
|
||||||
:is="componentFor(volume.source)"
|
:is="componentFor(volume.source)"
|
||||||
@@ -457,7 +406,7 @@ export default {
|
|||||||
:label="ucFirst(value.volumeBackups.error.message)"
|
:label="ucFirst(value.volumeBackups.error.message)"
|
||||||
/>
|
/>
|
||||||
<Banner
|
<Banner
|
||||||
v-if="!isLHV2VolExpansionFeatureEnabled && isLonghornV2(volume) && !isView"
|
v-if="isLonghornV2(volume) && !isView"
|
||||||
color="warning"
|
color="warning"
|
||||||
:label="t('harvester.volume.longhorn.disableResize')"
|
:label="t('harvester.volume.longhorn.disableResize')"
|
||||||
/>
|
/>
|
||||||
@@ -542,24 +491,25 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang='scss' scoped>
|
<style lang='scss' scoped>
|
||||||
.box-title {
|
.box {
|
||||||
display: flex;
|
position: relative;
|
||||||
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;
|
||||||
|
|||||||
+8
-6
@@ -99,6 +99,7 @@ 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) {
|
||||||
@@ -110,16 +111,17 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return isAvailable && pvc.isAvailable;
|
if (pvc.attachVM && isAvailable && pvc.attachVM?.id === this.vm?.id && this.isEdit) {
|
||||||
|
isBeingUsed = false;
|
||||||
|
} else if (pvc.attachVM) {
|
||||||
|
isBeingUsed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAvailable && !isBeingUsed && pvc.isAvailable;
|
||||||
})
|
})
|
||||||
.map((pvc) => {
|
.map((pvc) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
+5
-77
@@ -14,7 +14,6 @@ import { _VIEW } from '@shell/config/query-params';
|
|||||||
import LabelValue from '@shell/components/LabelValue';
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
import { ucFirst } from '@shell/utils/string';
|
import { ucFirst } from '@shell/utils/string';
|
||||||
import { GIBIBYTE } from '../../../../utils/unit';
|
import { GIBIBYTE } from '../../../../utils/unit';
|
||||||
import { EMPTY_IMAGE } from '../../../../utils/vm';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditVMImage',
|
name: 'HarvesterEditVMImage',
|
||||||
@@ -97,20 +96,8 @@ export default {
|
|||||||
return this.mode === _VIEW;
|
return this.mode === _VIEW;
|
||||||
},
|
},
|
||||||
|
|
||||||
isExistingCdrom() {
|
|
||||||
return this.value.type === 'cd-rom' && !this.value.newCreateId;
|
|
||||||
},
|
|
||||||
|
|
||||||
isEmptyImage() {
|
|
||||||
return this.value.image === EMPTY_IMAGE;
|
|
||||||
},
|
|
||||||
|
|
||||||
isHotplugCdRomFeatureEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('hotplugCdRom');
|
|
||||||
},
|
|
||||||
|
|
||||||
imagesOption() {
|
imagesOption() {
|
||||||
const images = this.images
|
return this.images
|
||||||
.filter((image) => {
|
.filter((image) => {
|
||||||
if (!image.isReady) return false;
|
if (!image.isReady) return false;
|
||||||
|
|
||||||
@@ -127,19 +114,6 @@ export default {
|
|||||||
value: image.id,
|
value: image.id,
|
||||||
disabled: image.isImportedImage
|
disabled: image.isImportedImage
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const options = [];
|
|
||||||
|
|
||||||
if (this.isHotplugCdRomFeatureEnabled) {
|
|
||||||
options.push({
|
|
||||||
label: this.t('harvester.virtualMachine.volume.emptyImage'),
|
|
||||||
value: EMPTY_IMAGE,
|
|
||||||
disabled: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
options.push(...images);
|
|
||||||
|
|
||||||
return options;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
imageName() {
|
imageName() {
|
||||||
@@ -205,7 +179,6 @@ export default {
|
|||||||
'value.type'(neu) {
|
'value.type'(neu) {
|
||||||
if (neu === 'cd-rom') {
|
if (neu === 'cd-rom') {
|
||||||
this.value['bus'] = 'sata';
|
this.value['bus'] = 'sata';
|
||||||
this.updateHotpluggable();
|
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -248,48 +221,12 @@ export default {
|
|||||||
|
|
||||||
return label;
|
return label;
|
||||||
},
|
},
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
this.value.hasDiskError = this.showDiskTooSmallError;
|
this.value.hasDiskError = this.showDiskTooSmallError;
|
||||||
this.$emit('update');
|
this.$emit('update');
|
||||||
},
|
},
|
||||||
|
|
||||||
updateHotpluggable() {
|
|
||||||
if (this.value.type !== 'cd-rom') {
|
|
||||||
this.value['hotpluggable'] = false;
|
|
||||||
} else {
|
|
||||||
this.value['hotpluggable'] = (this.value.bus === 'sata');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onTypeChange() {
|
|
||||||
if (this.value.image === EMPTY_IMAGE && this.value.type !== 'cd-rom') {
|
|
||||||
this.value['image'] = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateHotpluggable();
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
onBusChange() {
|
|
||||||
if (this.value.image === EMPTY_IMAGE && this.value.bus !== 'sata') {
|
|
||||||
this.value['image'] = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateHotpluggable();
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
onImageChange() {
|
onImageChange() {
|
||||||
if (this.value.image === EMPTY_IMAGE) {
|
|
||||||
this.value['type'] = 'cd-rom';
|
|
||||||
this.value['bus'] = 'sata';
|
|
||||||
this.value['size'] = `0${ GIBIBYTE }`;
|
|
||||||
this.update();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageResource = this.$store.getters['harvester/all'](HCI.IMAGE)?.find( (I) => this.value.image === I.id);
|
const imageResource = this.$store.getters['harvester/all'](HCI.IMAGE)?.find( (I) => this.value.image === I.id);
|
||||||
const isIsoImage = /iso$/i.test(imageResource?.imageSuffix);
|
const isIsoImage = /iso$/i.test(imageResource?.imageSuffix);
|
||||||
const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize);
|
const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize);
|
||||||
@@ -302,8 +239,6 @@ export default {
|
|||||||
this.value['bus'] = 'virtio';
|
this.value['bus'] = 'virtio';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateHotpluggable();
|
|
||||||
|
|
||||||
if (imageSize) {
|
if (imageSize) {
|
||||||
let imageSizeGiB = Math.ceil(imageSize / 1024 / 1024 / 1024);
|
let imageSizeGiB = Math.ceil(imageSize / 1024 / 1024 / 1024);
|
||||||
|
|
||||||
@@ -321,10 +256,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
checkImageExists(imageId) {
|
checkImageExists(imageId) {
|
||||||
if (imageId === EMPTY_IMAGE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!!imageId && this.imagesOption.length > 0 && !findBy(this.imagesOption, 'value', imageId)) {
|
if (!!imageId && this.imagesOption.length > 0 && !findBy(this.imagesOption, 'value', imageId)) {
|
||||||
this.$store.dispatch('growl/error', {
|
this.$store.dispatch('growl/error', {
|
||||||
title: this.$store.getters['i18n/t']('harvester.vmTemplate.tips.notExistImage.title', { name: imageId }),
|
title: this.$store.getters['i18n/t']('harvester.vmTemplate.tips.notExistImage.title', { name: imageId }),
|
||||||
@@ -352,7 +283,6 @@ export default {
|
|||||||
>
|
>
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-model:value="value.name"
|
v-model:value="value.name"
|
||||||
:disabled="!isCreate && isExistingCdrom"
|
|
||||||
:label="t('harvester.fields.name')"
|
:label="t('harvester.fields.name')"
|
||||||
required
|
required
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
@@ -372,11 +302,10 @@ export default {
|
|||||||
>
|
>
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="value.type"
|
v-model:value="value.type"
|
||||||
:disabled="!isCreate && isExistingCdrom"
|
|
||||||
:label="t('harvester.fields.type')"
|
:label="t('harvester.fields.type')"
|
||||||
:options="VOLUME_TYPE"
|
:options="VOLUME_TYPE"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
@update:value="onTypeChange"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
</InputOrDisplay>
|
</InputOrDisplay>
|
||||||
</div>
|
</div>
|
||||||
@@ -394,7 +323,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="value.image"
|
v-model:value="value.image"
|
||||||
:disabled="(idx === 0 || isExistingCdrom) && (!isCreate && !value.newCreateId && isVirtualType)"
|
:disabled="idx === 0 && !isCreate && !value.newCreateId && isVirtualType"
|
||||||
:label="t('harvester.fields.image')"
|
:label="t('harvester.fields.image')"
|
||||||
:options="imagesOption"
|
:options="imagesOption"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
@@ -422,7 +351,7 @@ export default {
|
|||||||
:label="t('harvester.fields.size')"
|
:label="t('harvester.fields.size')"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:required="validateRequired"
|
:required="validateRequired"
|
||||||
:disabled="isResizeDisabled || isEmptyImage || (!isCreate && isExistingCdrom)"
|
:disabled="isResizeDisabled"
|
||||||
:suffix="GIBIBYTE"
|
:suffix="GIBIBYTE"
|
||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
@@ -445,8 +374,7 @@ export default {
|
|||||||
:label="t('harvester.virtualMachine.volume.bus')"
|
:label="t('harvester.virtualMachine.volume.bus')"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:options="InterfaceOption"
|
:options="InterfaceOption"
|
||||||
:disabled="!isCreate && isExistingCdrom"
|
@update:value="update"
|
||||||
@update:value="onBusChange"
|
|
||||||
/>
|
/>
|
||||||
</InputOrDisplay>
|
</InputOrDisplay>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import PciDevices from './VirtualMachinePciDevices/index';
|
|||||||
import AccessCredentials from './VirtualMachineAccessCredentials';
|
import AccessCredentials from './VirtualMachineAccessCredentials';
|
||||||
import CloudConfig from './VirtualMachineCloudConfig';
|
import CloudConfig from './VirtualMachineCloudConfig';
|
||||||
import CpuMemory from './VirtualMachineCpuMemory';
|
import CpuMemory from './VirtualMachineCpuMemory';
|
||||||
import CpuModel from './VirtualMachineCpuModel';
|
|
||||||
import Network from './VirtualMachineNetwork';
|
import Network from './VirtualMachineNetwork';
|
||||||
import Volume from './VirtualMachineVolume';
|
import Volume from './VirtualMachineVolume';
|
||||||
import SSHKey from './VirtualMachineSSHKey';
|
import SSHKey from './VirtualMachineSSHKey';
|
||||||
@@ -58,7 +57,6 @@ export default {
|
|||||||
SSHKey,
|
SSHKey,
|
||||||
Network,
|
Network,
|
||||||
CpuMemory,
|
CpuMemory,
|
||||||
CpuModel,
|
|
||||||
CloudConfig,
|
CloudConfig,
|
||||||
NodeScheduling,
|
NodeScheduling,
|
||||||
PodAffinity,
|
PodAffinity,
|
||||||
@@ -98,6 +96,7 @@ export default {
|
|||||||
templateVersionId: '',
|
templateVersionId: '',
|
||||||
namePrefix: '',
|
namePrefix: '',
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
|
useTemplate: false,
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
hostname,
|
hostname,
|
||||||
isRestartImmediately,
|
isRestartImmediately,
|
||||||
@@ -211,10 +210,6 @@ export default {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
vGPUAsPCIDeviceEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
|
|
||||||
},
|
|
||||||
usbPassthroughEnabled() {
|
usbPassthroughEnabled() {
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
|
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
|
||||||
},
|
},
|
||||||
@@ -260,7 +255,6 @@ 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
|
||||||
@@ -344,15 +338,6 @@ 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 {
|
||||||
@@ -360,18 +345,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
validateCPUMemory() {
|
|
||||||
const { cpu, memory } = this;
|
|
||||||
|
|
||||||
if ((!cpu)) {
|
|
||||||
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.cpu') }, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((!memory)) {
|
|
||||||
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.memory') }, true));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async saveSingle(buttonCb) {
|
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;
|
||||||
@@ -440,44 +413,38 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_compareDisksIgnoreStorage(disksA = [], disksB = []) {
|
restartVM() {
|
||||||
if (disksA.length !== disksB.length) return false;
|
if (this.mode !== 'edit') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.value.isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cloneDeepNewVM = clone(this.value);
|
||||||
|
|
||||||
const sortedA = [...disksA].sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
|
// new VM
|
||||||
const sortedB = [...disksB].sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
|
delete cloneDeepNewVM?.metadata;
|
||||||
|
delete cloneDeepNewVM?.__clone;
|
||||||
|
|
||||||
for (let i = 0; i < sortedA.length; i++) {
|
// old VM
|
||||||
const cloneA = clone(sortedA[i]);
|
delete this.cloneVM?.metadata;
|
||||||
const cloneB = clone(sortedB[i]);
|
delete this.cloneVM?.__clone;
|
||||||
|
|
||||||
// remove the storage field before comparison
|
// add empty hostDevices to old VM as CRD does not have it.
|
||||||
delete cloneA?.spec?.resources?.requests?.storage;
|
const devicesObj = this.cloneVM?.spec?.template?.spec?.domain?.devices;
|
||||||
delete cloneB?.spec?.resources?.requests?.storage;
|
|
||||||
|
|
||||||
if (!isEqual(cloneA, cloneB)) return false;
|
if (devicesObj && devicesObj.hostDevices === undefined) {
|
||||||
|
set(devicesObj, 'hostDevices', []);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
const oldVM = JSON.parse(JSON.stringify(this.cloneVM));
|
||||||
},
|
const newVM = JSON.parse(JSON.stringify(cloneDeepNewVM));
|
||||||
|
|
||||||
_shouldPromptRestart(oldVM, newVM) {
|
// we won't show restart dialog in yaml page as we don't have a way to detect change in yaml editor.
|
||||||
const specChanged = !isEqual(oldVM?.spec, newVM?.spec);
|
// only check VM is changed in form page.
|
||||||
|
if (isEqual(oldVM, newVM)) {
|
||||||
// check if the disk defined in annotations has changed, ignoring storage size
|
return;
|
||||||
const oldDisks = parseVolumeClaimTemplates(oldVM);
|
}
|
||||||
const newDisks = parseVolumeClaimTemplates(newVM);
|
|
||||||
const diskChanged = !this._compareDisksIgnoreStorage(oldDisks, newDisks);
|
|
||||||
|
|
||||||
return specChanged || diskChanged;
|
|
||||||
},
|
|
||||||
|
|
||||||
restartVM() {
|
|
||||||
if (this.mode !== 'edit' || !this.value.isRunning) return;
|
|
||||||
|
|
||||||
const needsRestart = this._shouldPromptRestart(this.cloneVM, this.value);
|
|
||||||
|
|
||||||
// if no restart is needed (either no changes or only size changed)
|
|
||||||
if (!needsRestart) return;
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.isOpen = true;
|
this.isOpen = true;
|
||||||
@@ -496,11 +463,12 @@ 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const errors = this.getAccessCredentialsValidation();
|
const cpuMemoryErrors = this.getCPUMemoryValidation();
|
||||||
|
const accessCredentialsErrors = this.getAccessCredentialsValidation();
|
||||||
|
const errors = [...cpuMemoryErrors, ...accessCredentialsErrors];
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
return Promise.reject(errors);
|
return Promise.reject(errors);
|
||||||
@@ -544,18 +512,6 @@ export default {
|
|||||||
|
|
||||||
return out;
|
return out;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateCpuModel(value) {
|
|
||||||
if (!this.spec?.template?.spec?.domain?.cpu) {
|
|
||||||
set(this.spec, 'template.spec.domain.cpu', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value && value !== '') {
|
|
||||||
set(this.spec.template.spec.domain.cpu, 'model', value);
|
|
||||||
} else {
|
|
||||||
delete this.spec.template.spec.domain.cpu.model;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -744,7 +700,7 @@ export default {
|
|||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
v-if="enabledSriovgpu && !vGPUAsPCIDeviceEnabled"
|
v-if="enabledSriovgpu"
|
||||||
:label="t('harvester.tab.vGpuDevices')"
|
:label="t('harvester.tab.vGpuDevices')"
|
||||||
name="vGpuDevices"
|
name="vGpuDevices"
|
||||||
:weight="-6"
|
:weight="-6"
|
||||||
@@ -888,16 +844,6 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<CpuModel
|
|
||||||
v-model:value="cpuModel"
|
|
||||||
:mode="mode"
|
|
||||||
@update:value="updateCpuModel"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-20">
|
<div class="row mb-20">
|
||||||
<a
|
<a
|
||||||
v-if="showAdvanced"
|
v-if="showAdvanced"
|
||||||
@@ -927,10 +873,7 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="col span-6">
|
||||||
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,10 +39,6 @@ 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;
|
||||||
},
|
},
|
||||||
@@ -145,7 +141,6 @@ 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,26 +66,17 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
namespaceOptions() {
|
namespaceOptions() {
|
||||||
const namespaces = this.filteredNamespaces;
|
const out = (this.filteredNamespaces || []).map((namespace) => {
|
||||||
const selected = this.rows.map((r) => r?.namespace);
|
return {
|
||||||
|
label: namespace.metadata.name,
|
||||||
if (this.isStandaloneHarvester) {
|
value: namespace.id,
|
||||||
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() {
|
||||||
|
|||||||
@@ -1,357 +0,0 @@
|
|||||||
<script>
|
|
||||||
import CruResource from '@shell/components/CruResource';
|
|
||||||
import Tabbed from '@shell/components/Tabbed';
|
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
|
||||||
import UnitInput from '@shell/components/form/UnitInput';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
import FormValidation from '@shell/mixins/form-validation';
|
|
||||||
import { SECRET } from '@shell/config/types';
|
|
||||||
import { randomStr } from '@shell/utils/string';
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'EditOpenstackSource',
|
|
||||||
|
|
||||||
emits: ['update:value'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
CruResource,
|
|
||||||
Tabbed,
|
|
||||||
Tab,
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
NameNsDescription,
|
|
||||||
RadioGroup,
|
|
||||||
UnitInput
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView, FormValidation],
|
|
||||||
|
|
||||||
inheritAttrs: false,
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
this.allSecrets = await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
if (!this.value.spec) this.value.spec = {};
|
|
||||||
if (!this.value.spec.credentials) this.value.spec.credentials = {};
|
|
||||||
|
|
||||||
const initialMode = this.value.spec.credentials.name ? 'existing' : 'new';
|
|
||||||
|
|
||||||
return {
|
|
||||||
allSecrets: [],
|
|
||||||
authMode: initialMode,
|
|
||||||
|
|
||||||
newUsername: '',
|
|
||||||
newPassword: '',
|
|
||||||
newProjectName: '',
|
|
||||||
newDomainName: '',
|
|
||||||
newCaCert: '',
|
|
||||||
|
|
||||||
// Rules for fields that exist in the value object (Model)
|
|
||||||
fvFormRuleSets: [
|
|
||||||
{ path: 'metadata.name', rules: ['nameRequired'] },
|
|
||||||
{ path: 'spec.endpoint', rules: ['endpointRequired'] },
|
|
||||||
{ path: 'spec.region', rules: ['regionRequired'] },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
authModeOptions() {
|
|
||||||
return [
|
|
||||||
{ label: this.t('harvester.addons.vmImport.fields.createSecret'), value: 'new' },
|
|
||||||
{ label: this.t('harvester.addons.vmImport.fields.useSecret'), value: 'existing' }
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
secretOptions() {
|
|
||||||
const currentNamespace = this.value.metadata.namespace || 'default';
|
|
||||||
|
|
||||||
return this.allSecrets
|
|
||||||
.filter((s) => s.metadata.namespace === currentNamespace)
|
|
||||||
.map((s) => ({
|
|
||||||
label: s.nameDisplay,
|
|
||||||
value: s.metadata.name
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
// Define custom rules for the FormValidation mixin
|
|
||||||
fvExtraRules() {
|
|
||||||
return {
|
|
||||||
nameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.fields.name') }) : undefined,
|
|
||||||
endpointRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.openstack.fields.endpoint') }) : undefined,
|
|
||||||
regionRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.openstack.fields.region') }) : undefined,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
// Combine mixin validation + conditional manual checks
|
|
||||||
isFormValid() {
|
|
||||||
// Check static fields via Mixin
|
|
||||||
if (!this.fvFormIsValid) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check conditional fields
|
|
||||||
if (this.authMode === 'new') {
|
|
||||||
if (!this.newUsername || !this.newPassword) return false;
|
|
||||||
if (!this.newProjectName || !this.newDomainName) return false;
|
|
||||||
} else {
|
|
||||||
if (!this.value.spec.credentials.name) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
usernameRule(val) {
|
|
||||||
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.username') }) : undefined;
|
|
||||||
},
|
|
||||||
passwordRule(val) {
|
|
||||||
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.password') }) : undefined;
|
|
||||||
},
|
|
||||||
projectRule(val) {
|
|
||||||
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.openstack.fields.projectName') }) : undefined;
|
|
||||||
},
|
|
||||||
domainRule(val) {
|
|
||||||
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.openstack.fields.domainName') }) : undefined;
|
|
||||||
},
|
|
||||||
secretRule(val) {
|
|
||||||
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.selectSecret') }) : undefined;
|
|
||||||
},
|
|
||||||
|
|
||||||
async saveSource(buttonCb) {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.authMode === 'new') {
|
|
||||||
const secretName = `${ this.value.metadata.name }-creds-${ randomStr(4).toLowerCase() }`;
|
|
||||||
const namespace = this.value.metadata.namespace || 'default';
|
|
||||||
|
|
||||||
const newSecret = await this.$store.dispatch(`${ inStore }/create`, {
|
|
||||||
type: SECRET,
|
|
||||||
metadata: {
|
|
||||||
name: secretName,
|
|
||||||
namespace
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
newSecret['_type'] = 'Opaque';
|
|
||||||
newSecret['data'] = {
|
|
||||||
username: btoa(this.newUsername),
|
|
||||||
password: btoa(this.newPassword),
|
|
||||||
project_name: btoa(this.newProjectName),
|
|
||||||
domain_name: btoa(this.newDomainName),
|
|
||||||
ca_cert: this.newCaCert ? btoa(this.newCaCert) : undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
await newSecret.save();
|
|
||||||
|
|
||||||
this.value.spec.credentials = {
|
|
||||||
name: secretName,
|
|
||||||
namespace
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.save(buttonCb);
|
|
||||||
} catch (err) {
|
|
||||||
this.errors = [err];
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CruResource
|
|
||||||
:done-route="doneRoute"
|
|
||||||
:resource="value"
|
|
||||||
:mode="mode"
|
|
||||||
:errors="errors"
|
|
||||||
:apply-hooks="applyHooks"
|
|
||||||
:validation-passed="isFormValid"
|
|
||||||
@finish="saveSource"
|
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
|
||||||
<NameNsDescription
|
|
||||||
:value="value"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="{ name: fvGetAndReportPathRules('metadata.name') }"
|
|
||||||
@update:value="$emit('update:value', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Tabbed
|
|
||||||
v-bind="$attrs"
|
|
||||||
class="mt-15"
|
|
||||||
:side-tabs="true"
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
name="basic"
|
|
||||||
:label="t('harvester.addons.vmImport.titles.basic')"
|
|
||||||
:weight="3"
|
|
||||||
>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="value.spec.endpoint"
|
|
||||||
:label="t('harvester.addons.vmImport.openstack.fields.endpoint')"
|
|
||||||
:placeholder="t('harvester.addons.vmImport.openstack.placeholders.endpoint')"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="fvGetAndReportPathRules('spec.endpoint')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="value.spec.region"
|
|
||||||
:label="t('harvester.addons.vmImport.openstack.fields.region')"
|
|
||||||
:placeholder="t('harvester.addons.vmImport.openstack.placeholders.region')"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="fvGetAndReportPathRules('spec.region')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
|
||||||
name="auth"
|
|
||||||
:label="t('harvester.addons.vmImport.titles.auth')"
|
|
||||||
:weight="2"
|
|
||||||
>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-12">
|
|
||||||
<RadioGroup
|
|
||||||
v-model:value="authMode"
|
|
||||||
name="authMode"
|
|
||||||
:options="authModeOptions"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="authMode === 'new'">
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="newUsername"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.username')"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="[usernameRule]"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="newPassword"
|
|
||||||
type="password"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.password')"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="[passwordRule]"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="newProjectName"
|
|
||||||
:label="t('harvester.addons.vmImport.openstack.fields.projectName')"
|
|
||||||
:placeholder="t('harvester.addons.vmImport.openstack.placeholders.projectName')"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="[projectRule]"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="newDomainName"
|
|
||||||
:label="t('harvester.addons.vmImport.openstack.fields.domainName')"
|
|
||||||
:placeholder="t('harvester.addons.vmImport.openstack.placeholders.domainName')"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="[domainRule]"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="newCaCert"
|
|
||||||
type="multiline"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.caCert')"
|
|
||||||
:placeholder="t('harvester.addons.vmImport.placeholders.caCert')"
|
|
||||||
:min-height="100"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="authMode === 'existing'">
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="value.spec.credentials.name"
|
|
||||||
:options="secretOptions"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.selectSecret')"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="[secretRule]"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
|
||||||
name="advanced"
|
|
||||||
:label="t('harvester.addons.vmImport.titles.advanced')"
|
|
||||||
:weight="1"
|
|
||||||
>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="value.spec.uploadImageRetryCount"
|
|
||||||
:label="t('harvester.addons.vmImport.openstack.fields.retryCount')"
|
|
||||||
:placeholder="t('harvester.addons.vmImport.openstack.placeholders.retryCount')"
|
|
||||||
suffix="Times"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-6">
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="value.spec.uploadImageRetryDelay"
|
|
||||||
:label="t('harvester.addons.vmImport.openstack.fields.retryDelay')"
|
|
||||||
:placeholder="t('harvester.addons.vmImport.openstack.placeholders.retryDelay')"
|
|
||||||
suffix="Seconds"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
</Tabbed>
|
|
||||||
</CruResource>
|
|
||||||
</template>
|
|
||||||
@@ -1,322 +0,0 @@
|
|||||||
<script>
|
|
||||||
import CruResource from '@shell/components/CruResource';
|
|
||||||
import Tabbed from '@shell/components/Tabbed';
|
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
|
||||||
import UnitInput from '@shell/components/form/UnitInput';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
import FormValidation from '@shell/mixins/form-validation';
|
|
||||||
import { SECRET } from '@shell/config/types';
|
|
||||||
import { randomStr } from '@shell/utils/string';
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'EditOvaSource',
|
|
||||||
|
|
||||||
emits: ['update:value'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
CruResource,
|
|
||||||
Tabbed,
|
|
||||||
Tab,
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
NameNsDescription,
|
|
||||||
RadioGroup,
|
|
||||||
UnitInput
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView, FormValidation],
|
|
||||||
|
|
||||||
inheritAttrs: false,
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
this.allSecrets = await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
if (!this.value.spec) this.value.spec = {};
|
|
||||||
|
|
||||||
// Auth is optional for OVA (public URLs).
|
|
||||||
// If credentials.name exists -> Existing.
|
|
||||||
// If not -> None (default).
|
|
||||||
let initialMode = 'none';
|
|
||||||
|
|
||||||
if (this.value.spec.credentials?.name) {
|
|
||||||
initialMode = 'existing';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
allSecrets: [],
|
|
||||||
authMode: initialMode,
|
|
||||||
|
|
||||||
newUsername: '',
|
|
||||||
newPassword: '',
|
|
||||||
newCaCert: '', // Key will be "ca.crt"
|
|
||||||
|
|
||||||
// Validation Rules for static fields
|
|
||||||
fvFormRuleSets: [
|
|
||||||
{ path: 'metadata.name', rules: ['nameRequired'] },
|
|
||||||
{ path: 'spec.url', rules: ['urlRequired'] },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
authModeOptions() {
|
|
||||||
return [
|
|
||||||
{ label: this.t('harvester.addons.vmImport.fields.none'), value: 'none' },
|
|
||||||
{ label: this.t('harvester.addons.vmImport.fields.createSecret'), value: 'new' },
|
|
||||||
{ label: this.t('harvester.addons.vmImport.fields.useSecret'), value: 'existing' }
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
secretOptions() {
|
|
||||||
const currentNamespace = this.value.metadata.namespace || 'default';
|
|
||||||
|
|
||||||
return this.allSecrets
|
|
||||||
.filter((s) => s.metadata.namespace === currentNamespace)
|
|
||||||
.map((s) => ({
|
|
||||||
label: s.nameDisplay,
|
|
||||||
value: s.metadata.name
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
// Define custom rules for the FormValidation mixin
|
|
||||||
fvExtraRules() {
|
|
||||||
return {
|
|
||||||
nameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.fields.name') }) : undefined,
|
|
||||||
urlRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.ova.fields.url') }) : undefined,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
isFormValid() {
|
|
||||||
if (!this.fvFormIsValid) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.authMode === 'new') {
|
|
||||||
// At least a username/password OR a CA cert to be provided.
|
|
||||||
// If the user selected "Create New", they likely intend to enter something.
|
|
||||||
if (!this.newUsername && !this.newPassword && !this.newCaCert) return false;
|
|
||||||
} else if (this.authMode === 'existing') {
|
|
||||||
if (!this.value.spec.credentials?.name) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
authMode(newMode) {
|
|
||||||
if (newMode === 'existing') {
|
|
||||||
// Bind to value.spec.credentials.name for existing credential
|
|
||||||
// Ensure 'credentials' object exists first when selected
|
|
||||||
if (!this.value.spec.credentials) {
|
|
||||||
this.value.spec.credentials = {
|
|
||||||
name: '',
|
|
||||||
namespace: this.value.metadata.namespace || 'default'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
secretRule(val) {
|
|
||||||
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.selectSecret') }) : undefined;
|
|
||||||
},
|
|
||||||
|
|
||||||
async saveSource(buttonCb) {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.authMode === 'none') {
|
|
||||||
// Clear any credential reference
|
|
||||||
delete this.value.spec.credentials;
|
|
||||||
} else if (this.authMode === 'new') {
|
|
||||||
const secretName = `${ this.value.metadata.name }-creds-${ randomStr(4).toLowerCase() }`;
|
|
||||||
const namespace = this.value.metadata.namespace || 'default';
|
|
||||||
|
|
||||||
const newSecret = await this.$store.dispatch(`${ inStore }/create`, {
|
|
||||||
type: SECRET,
|
|
||||||
metadata: {
|
|
||||||
name: secretName,
|
|
||||||
namespace
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
newSecret['_type'] = 'Opaque';
|
|
||||||
newSecret['data'] = {
|
|
||||||
// Optional fields logic
|
|
||||||
username: this.newUsername ? btoa(this.newUsername) : undefined,
|
|
||||||
password: this.newPassword ? btoa(this.newPassword) : undefined,
|
|
||||||
// vm-import-controller code specifies "ca.crt" with a dot.
|
|
||||||
'ca.crt': this.newCaCert ? btoa(this.newCaCert) : undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
await newSecret.save();
|
|
||||||
|
|
||||||
this.value.spec.credentials = {
|
|
||||||
name: secretName,
|
|
||||||
namespace
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.save(buttonCb);
|
|
||||||
} catch (err) {
|
|
||||||
this.errors = [err];
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CruResource
|
|
||||||
:done-route="doneRoute"
|
|
||||||
:resource="value"
|
|
||||||
:mode="mode"
|
|
||||||
:errors="errors"
|
|
||||||
:apply-hooks="applyHooks"
|
|
||||||
:validation-passed="isFormValid"
|
|
||||||
@finish="saveSource"
|
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
|
||||||
<NameNsDescription
|
|
||||||
:value="value"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="{ name: fvGetAndReportPathRules('metadata.name') }"
|
|
||||||
@update:value="$emit('update:value', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Tabbed
|
|
||||||
v-bind="$attrs"
|
|
||||||
class="mt-15"
|
|
||||||
:side-tabs="true"
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
name="basic"
|
|
||||||
:label="t('harvester.addons.vmImport.titles.basic')"
|
|
||||||
:weight="3"
|
|
||||||
>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="value.spec.url"
|
|
||||||
:label="t('harvester.addons.vmImport.ova.fields.url')"
|
|
||||||
:placeholder="t('harvester.addons.vmImport.ova.placeholders.url')"
|
|
||||||
tooltip="Supports HTTP and HTTPS protocols."
|
|
||||||
:mode="mode"
|
|
||||||
:rules="fvGetAndReportPathRules('spec.url')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
|
||||||
name="auth"
|
|
||||||
:label="t('harvester.addons.vmImport.titles.auth')"
|
|
||||||
:weight="2"
|
|
||||||
>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-12">
|
|
||||||
<RadioGroup
|
|
||||||
v-model:value="authMode"
|
|
||||||
name="authMode"
|
|
||||||
:options="authModeOptions"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="authMode === 'new'">
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="newUsername"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.username')"
|
|
||||||
placeholder="(Optional)"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="newPassword"
|
|
||||||
type="password"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.password')"
|
|
||||||
placeholder="(Optional)"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="newCaCert"
|
|
||||||
type="multiline"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.caCert')"
|
|
||||||
:placeholder="t('harvester.addons.vmImport.placeholders.caCert')"
|
|
||||||
:min-height="100"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="authMode === 'existing'">
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="value.spec.credentials.name"
|
|
||||||
:options="secretOptions"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.selectSecret')"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="[secretRule]"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
|
||||||
name="advanced"
|
|
||||||
:label="t('harvester.addons.vmImport.titles.advanced')"
|
|
||||||
:weight="1"
|
|
||||||
>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-12">
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="value.spec.httpTimeoutSeconds"
|
|
||||||
:label="t('harvester.addons.vmImport.ova.fields.httpTimeout')"
|
|
||||||
:placeholder="t('harvester.addons.vmImport.ova.placeholders.httpTimeout')"
|
|
||||||
suffix="Seconds"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
</Tabbed>
|
|
||||||
</CruResource>
|
|
||||||
</template>
|
|
||||||
@@ -1,570 +0,0 @@
|
|||||||
<script>
|
|
||||||
import CruResource from '@shell/components/CruResource';
|
|
||||||
import Tabbed from '@shell/components/Tabbed';
|
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
import FormValidation from '@shell/mixins/form-validation';
|
|
||||||
import { STORAGE_CLASS, NETWORK_ATTACHMENT } from '@shell/config/types';
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
|
||||||
import { MANAGEMENT_NETWORK } from '../mixins/harvester-vm';
|
|
||||||
import { VMIMPORT_SOURCE_PROVIDER, VMIMPORT_SOURCE_KINDS } from '../config/types';
|
|
||||||
import { HCI } from '../types';
|
|
||||||
import { isValidDNSLabelName } from '@pkg/utils/regular';
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
// Full API types for the fetch dispatch
|
|
||||||
const VMWARE_SOURCE_TYPE = `${ HCI.MIGRATION }.${ VMIMPORT_SOURCE_KINDS.VMWARE.toLowerCase() }`;
|
|
||||||
const OPENSTACK_SOURCE_TYPE = `${ HCI.MIGRATION }.${ VMIMPORT_SOURCE_KINDS.OPENSTACK.toLowerCase() }`;
|
|
||||||
const OVA_SOURCE_TYPE = `${ HCI.MIGRATION }.${ VMIMPORT_SOURCE_KINDS.OVA.toLowerCase() }`;
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'EditVirtualMachineImport',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
CruResource,
|
|
||||||
Tabbed,
|
|
||||||
Tab,
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
NameNsDescription,
|
|
||||||
Checkbox
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView, FormValidation],
|
|
||||||
|
|
||||||
inheritAttrs: false,
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
// Fetch all dependencies in parallel to speed up the page load
|
|
||||||
const hash = {
|
|
||||||
storageClasses: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
|
||||||
networks: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }),
|
|
||||||
vmwareSources: this.$store.dispatch(`${ inStore }/findAll`, { type: VMWARE_SOURCE_TYPE }),
|
|
||||||
openstackSources: this.$store.dispatch(`${ inStore }/findAll`, { type: OPENSTACK_SOURCE_TYPE }),
|
|
||||||
ovaSources: this.$store.dispatch(`${ inStore }/findAll`, { type: OVA_SOURCE_TYPE }).catch(() => []),
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await allHash(hash);
|
|
||||||
|
|
||||||
this.allStorageClasses = res.storageClasses;
|
|
||||||
this.allNetworks = res.networks;
|
|
||||||
this.vmwareSources = res.vmwareSources;
|
|
||||||
this.openstackSources = res.openstackSources;
|
|
||||||
this.ovaSources = res.ovaSources;
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
// Ensure the spec object exists to prevent 'undefined' errors during rendering
|
|
||||||
if (!this.value.spec) this.value.spec = {};
|
|
||||||
if (!this.value.spec.sourceCluster) this.value.spec.sourceCluster = {};
|
|
||||||
if (!this.value.spec.networkMapping) this.value.spec.networkMapping = [];
|
|
||||||
|
|
||||||
// Detect if in Edit mode by checking the existing kind
|
|
||||||
// This allows to pre-select the correct Provider Type tab
|
|
||||||
let initialProvider = '';
|
|
||||||
const existingKind = this.value.spec.sourceCluster.kind;
|
|
||||||
|
|
||||||
if (existingKind === VMIMPORT_SOURCE_KINDS.VMWARE) initialProvider = VMIMPORT_SOURCE_PROVIDER.VMWARE;
|
|
||||||
else if (existingKind === VMIMPORT_SOURCE_KINDS.OPENSTACK) initialProvider = VMIMPORT_SOURCE_PROVIDER.OPENSTACK;
|
|
||||||
else if (existingKind === VMIMPORT_SOURCE_KINDS.OVA) initialProvider = VMIMPORT_SOURCE_PROVIDER.OVA;
|
|
||||||
|
|
||||||
// Construct the unique key (Kind/Namespace/Name) if we are editing an existing resource
|
|
||||||
let initialSourceKey = null;
|
|
||||||
|
|
||||||
if (this.value.spec.sourceCluster.name) {
|
|
||||||
initialSourceKey = `${ existingKind }/${ this.value.spec.sourceCluster.namespace }/${ this.value.spec.sourceCluster.name }`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
allStorageClasses: [],
|
|
||||||
allNetworks: [],
|
|
||||||
vmwareSources: [],
|
|
||||||
openstackSources: [],
|
|
||||||
ovaSources: [],
|
|
||||||
|
|
||||||
// UI State
|
|
||||||
sourceProviderType: initialProvider,
|
|
||||||
selectedSourceKey: initialSourceKey,
|
|
||||||
|
|
||||||
// Static Options
|
|
||||||
providerTypeOptions: [
|
|
||||||
{ label: 'VMware', value: VMIMPORT_SOURCE_PROVIDER.VMWARE },
|
|
||||||
{ label: 'OpenStack', value: VMIMPORT_SOURCE_PROVIDER.OPENSTACK },
|
|
||||||
{ label: 'OVA', value: VMIMPORT_SOURCE_PROVIDER.OVA }
|
|
||||||
],
|
|
||||||
diskBusOptions: [
|
|
||||||
// Allow resetting selection / reset to the default behavior (sending null/empty)
|
|
||||||
{ label: this.t('harvester.addons.vmImport.options.useDefault'), value: '' },
|
|
||||||
{ label: 'VirtIO', value: 'virtio' },
|
|
||||||
{ label: 'SCSI', value: 'scsi' },
|
|
||||||
{ label: 'SATA', value: 'sata' },
|
|
||||||
{ label: 'USB', value: 'usb' },
|
|
||||||
],
|
|
||||||
interfaceModelOptions: [
|
|
||||||
// Allow resetting selection / reset to the default behavior (sending null/empty)
|
|
||||||
{ label: this.t('harvester.addons.vmImport.options.useDefault'), value: '' },
|
|
||||||
{ label: 'VirtIO', value: 'virtio' },
|
|
||||||
{ label: 'e1000', value: 'e1000' },
|
|
||||||
{ label: 'e1000e', value: 'e1000e' },
|
|
||||||
{ label: 'ne2k_pci', value: 'ne2k_pci' },
|
|
||||||
{ label: 'pcnet', value: 'pcnet' },
|
|
||||||
{ label: 'rtl8139', value: 'rtl8139' },
|
|
||||||
],
|
|
||||||
|
|
||||||
fvFormRuleSets: [
|
|
||||||
{ path: 'metadata.name', rules: ['nameRequired'] },
|
|
||||||
{ path: 'spec.virtualMachineName', rules: ['vmNameRequired', 'rfc1123'] },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
if (this.registerBeforeHook) {
|
|
||||||
this.registerBeforeHook(this.updateBeforeSave);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
// Return only the sources that match the selected Provider Type (VMware or OpenStack)
|
|
||||||
sourceOptions() {
|
|
||||||
let list = [];
|
|
||||||
|
|
||||||
if (this.sourceProviderType === VMIMPORT_SOURCE_PROVIDER.VMWARE) {
|
|
||||||
list = this.vmwareSources;
|
|
||||||
} else if (this.sourceProviderType === VMIMPORT_SOURCE_PROVIDER.OPENSTACK) {
|
|
||||||
list = this.openstackSources;
|
|
||||||
} else if (this.sourceProviderType === VMIMPORT_SOURCE_PROVIDER.OVA) {
|
|
||||||
list = this.ovaSources;
|
|
||||||
}
|
|
||||||
|
|
||||||
return list.map((s) => {
|
|
||||||
// Fallback for API version/kind if missing on the object
|
|
||||||
let kind = s.kind;
|
|
||||||
|
|
||||||
if (!kind) {
|
|
||||||
if (this.sourceProviderType === VMIMPORT_SOURCE_PROVIDER.VMWARE) kind = VMIMPORT_SOURCE_KINDS.VMWARE;
|
|
||||||
else if (this.sourceProviderType === VMIMPORT_SOURCE_PROVIDER.OPENSTACK) kind = VMIMPORT_SOURCE_KINDS.OPENSTACK;
|
|
||||||
else if (this.sourceProviderType === VMIMPORT_SOURCE_PROVIDER.OVA) kind = VMIMPORT_SOURCE_KINDS.OVA;
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiVersion = s.apiVersion || `${ HCI.MIGRATION }/v1beta1`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
label: s.metadata.name,
|
|
||||||
value: `${ kind }/${ s.metadata.namespace }/${ s.metadata.name }`,
|
|
||||||
// We attach the raw metadata so we can easily populate the spec later without re-finding the object
|
|
||||||
raw: {
|
|
||||||
kind,
|
|
||||||
apiVersion,
|
|
||||||
name: s.metadata.name,
|
|
||||||
namespace: s.metadata.namespace
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
fvExtraRules() {
|
|
||||||
return {
|
|
||||||
nameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.fields.name') }) : undefined,
|
|
||||||
vmNameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.vmName') }) : undefined,
|
|
||||||
rfc1123:
|
|
||||||
(val) => {
|
|
||||||
if (val && !isValidDNSLabelName(val)) {
|
|
||||||
return this.t('harvester.addons.vmImport.errors.rfc1123');
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
// Perform various form validations before allowing to submit
|
|
||||||
isFormValid() {
|
|
||||||
// Check VM Name is valid
|
|
||||||
const nameError = this.fvNameRule(this.value.spec.virtualMachineName);
|
|
||||||
|
|
||||||
if (nameError) return false;
|
|
||||||
|
|
||||||
// Check mandatory fields in Basics
|
|
||||||
if (!this.value.spec.virtualMachineName) return false;
|
|
||||||
if (!this.selectedSourceKey) return false;
|
|
||||||
|
|
||||||
// Check Network Mappings
|
|
||||||
// If any row is missing source or destination, the form is invalid.
|
|
||||||
const networks = this.value.spec.networkMapping || [];
|
|
||||||
const hasInvalidRow = networks.some((row) => !row.sourceNetwork || !row.destinationNetwork);
|
|
||||||
|
|
||||||
if (hasInvalidRow) return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
isNetworkTabInvalid() {
|
|
||||||
const networks = this.value.spec.networkMapping || [];
|
|
||||||
// Only error if a row exists AND it is missing fields
|
|
||||||
|
|
||||||
return networks.some((row) => !row.sourceNetwork || !row.destinationNetwork);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Filter out internal storage classes
|
|
||||||
// to prevent selecting a class that might cause the import to fail
|
|
||||||
storageClassOptions() {
|
|
||||||
return this.allStorageClasses
|
|
||||||
.filter((sc) => {
|
|
||||||
const isInternal = sc.parameters?.['harvesterhci.io/isInternalStorageClass'] === 'true';
|
|
||||||
|
|
||||||
return !isInternal;
|
|
||||||
})
|
|
||||||
.map((sc) => ({
|
|
||||||
label: sc.nameDisplay,
|
|
||||||
value: sc.id
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
networkOptions() {
|
|
||||||
const mgmtOption = {
|
|
||||||
label: 'Management Network',
|
|
||||||
value: MANAGEMENT_NETWORK
|
|
||||||
};
|
|
||||||
|
|
||||||
const vlanOptions = this.allNetworks.map((n) => ({
|
|
||||||
label: n.nameDisplay || n.metadata.name,
|
|
||||||
value: n.id
|
|
||||||
}));
|
|
||||||
|
|
||||||
return [mgmtOption, ...vlanOptions];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
// Clear the selected cluster if the user switches providers (e.g. VMware -> OpenStack)
|
|
||||||
// Prevents submitting a VMware cluster name while the kind is OpenStack
|
|
||||||
onProviderTypeChange(newType) {
|
|
||||||
this.selectedSourceKey = null;
|
|
||||||
this.value.spec.sourceCluster = {};
|
|
||||||
},
|
|
||||||
|
|
||||||
// Update the sourceCluster object based on the single dropdown selection
|
|
||||||
updateSource(key) {
|
|
||||||
this.selectedSourceKey = key;
|
|
||||||
const selectedOption = this.sourceOptions.find((o) => o.value === key);
|
|
||||||
|
|
||||||
if (selectedOption) {
|
|
||||||
const {
|
|
||||||
kind, apiVersion, name, namespace
|
|
||||||
} = selectedOption.raw;
|
|
||||||
|
|
||||||
this.value.spec.sourceCluster = {
|
|
||||||
kind,
|
|
||||||
apiVersion,
|
|
||||||
name,
|
|
||||||
namespace
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
this.value.spec.sourceCluster = {};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addNetworkMapping() {
|
|
||||||
this.value.spec.networkMapping.push({
|
|
||||||
sourceNetwork: '',
|
|
||||||
destinationNetwork: '',
|
|
||||||
networkInterfaceModel: ''
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
removeNetworkMapping(index) {
|
|
||||||
if (!this.value?.spec?.networkMapping) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index >= 0 && index < this.value.spec.networkMapping.length) {
|
|
||||||
this.value.spec.networkMapping.splice(index, 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
requiredRule(val) {
|
|
||||||
if (!val) {
|
|
||||||
return this.t('validation.required', { key: this.t('generic.value') });
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Validates that the input follows Kubernetes Naming Rules (RFC 1123).
|
|
||||||
// If the source VM has uppercase letters or spaces, the user must be warned
|
|
||||||
// that they cannot import it until they rename it on the source. See:
|
|
||||||
// https://docs.harvesterhci.io/v1.6/advanced/addons/vmimport/#source-virtual-machine-name-is-not-rfc1123-compliant
|
|
||||||
fvNameRule(val) {
|
|
||||||
if (!val) return undefined; // 'Required' check handles empty state separately
|
|
||||||
|
|
||||||
// valid RFC 1123
|
|
||||||
if (!isValidDNSLabelName(val)) {
|
|
||||||
return this.t('harvester.addons.vmImport.errors.rfc1123');
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
|
|
||||||
updateBeforeSave() {
|
|
||||||
// If networkMapping exists, filter out the "Management Network" rows
|
|
||||||
// Let the vm-import-controller set the default network mapping
|
|
||||||
if (this.value.spec.networkMapping) {
|
|
||||||
this.value.spec.networkMapping = this.value.spec.networkMapping.filter((row) => {
|
|
||||||
return row.destinationNetwork !== MANAGEMENT_NETWORK;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Only handles complex logic that doesn't fit into simple field rules
|
|
||||||
async saveOverride(buttonCb) {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
this.errors = [];
|
|
||||||
|
|
||||||
// Validate Provider Type
|
|
||||||
if (!this.sourceProviderType) {
|
|
||||||
errors.push(this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.sourceProvider') }));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate Network Tab
|
|
||||||
if (this.isNetworkTabInvalid) {
|
|
||||||
errors.push(this.t('harvester.addons.vmImport.errors.networkMappingRequired'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return immediately in case of an error, avoid that `this.save()` runs, preventing `updateBeforeSave` from resetting data.
|
|
||||||
if (errors.length > 0) {
|
|
||||||
this.errors = errors;
|
|
||||||
buttonCb(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only proceed if valid
|
|
||||||
this.save(buttonCb);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CruResource
|
|
||||||
:done-route="doneRoute"
|
|
||||||
:resource="value"
|
|
||||||
:mode="mode"
|
|
||||||
:errors="errors"
|
|
||||||
:apply-hooks="applyHooks"
|
|
||||||
:validation-passed="fvFormIsValid"
|
|
||||||
@finish="saveOverride"
|
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
|
||||||
<NameNsDescription
|
|
||||||
:value="value"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="{ name: fvGetAndReportPathRules('metadata.name') }"
|
|
||||||
@update:value="$emit('update:value', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Tabbed
|
|
||||||
v-bind="$attrs"
|
|
||||||
class="mt-15"
|
|
||||||
:side-tabs="true"
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
name="basic"
|
|
||||||
:label="t('harvester.addons.vmImport.titles.basic')"
|
|
||||||
:weight="3"
|
|
||||||
>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="sourceProviderType"
|
|
||||||
:options="providerTypeOptions"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.sourceProvider')"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="[requiredRule]"
|
|
||||||
required
|
|
||||||
@update:value="onProviderTypeChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledSelect
|
|
||||||
:value="selectedSourceKey"
|
|
||||||
:options="sourceOptions"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.sourceCluster')"
|
|
||||||
:placeholder="sourceProviderType ? t('harvester.addons.vmImport.placeholders.selectCluster') : t('harvester.addons.vmImport.placeholders.selectProviderFirst')"
|
|
||||||
:disabled="!sourceProviderType"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="fvGetAndReportPathRules('selectedSourceKey')"
|
|
||||||
required
|
|
||||||
@update:value="updateSource"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="value.spec.virtualMachineName"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.vmName')"
|
|
||||||
:placeholder="t('harvester.addons.vmImport.placeholders.matchSource')"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="fvGetAndReportPathRules('spec.virtualMachineName')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="value.spec.storageClass"
|
|
||||||
:options="storageClassOptions"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.targetStorageClass')"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
|
||||||
name="networking"
|
|
||||||
:label="t('harvester.addons.vmImport.titles.networking')"
|
|
||||||
:weight="2"
|
|
||||||
:error="isNetworkTabInvalid"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="(row, i) in value.spec.networkMapping"
|
|
||||||
:key="i"
|
|
||||||
class="network-row box mb-10"
|
|
||||||
>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-4">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="row.sourceNetwork"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.sourceNetwork')"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="[requiredRule]"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-4">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="row.destinationNetwork"
|
|
||||||
:options="networkOptions"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.destNetwork')"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="[requiredRule]"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-3">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="row.networkInterfaceModel"
|
|
||||||
:options="interfaceModelOptions"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.interfaceModel')"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-1 remove-btn-container">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn role-link"
|
|
||||||
@click="removeNetworkMapping(i)"
|
|
||||||
>
|
|
||||||
{{ t('harvester.addons.vmImport.actions.remove') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn role-secondary"
|
|
||||||
@click="addNetworkMapping"
|
|
||||||
>
|
|
||||||
{{ t('harvester.addons.vmImport.actions.addNetwork') }}
|
|
||||||
</button>
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
|
||||||
name="advanced"
|
|
||||||
:label="t('harvester.addons.vmImport.titles.advanced')"
|
|
||||||
:weight="1"
|
|
||||||
>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="value.spec.folder"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.folder')"
|
|
||||||
:placeholder="t('harvester.addons.vmImport.placeholders.folderExample')"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="value.spec.defaultDiskBusType"
|
|
||||||
:options="diskBusOptions"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.diskBus')"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="value.spec.defaultNetworkInterfaceModel"
|
|
||||||
:options="interfaceModelOptions"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.defaultInterface')"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<Checkbox
|
|
||||||
v-model:value="value.spec.skipPreflightChecks"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.skipPreflight')"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
v-model:value="value.spec.forcePowerOff"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.forcePowerOff')"
|
|
||||||
:mode="mode"
|
|
||||||
class="mt-10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
</Tabbed>
|
|
||||||
</CruResource>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.network-row {
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
background: var(--body-bg);
|
|
||||||
}
|
|
||||||
.remove-btn-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
<script>
|
|
||||||
import CruResource from '@shell/components/CruResource';
|
|
||||||
import Tabbed from '@shell/components/Tabbed';
|
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
import FormValidation from '@shell/mixins/form-validation';
|
|
||||||
import { SECRET } from '@shell/config/types';
|
|
||||||
import { randomStr } from '@shell/utils/string';
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'EditVmwareSource',
|
|
||||||
|
|
||||||
// Declare the event, fixes a console warning
|
|
||||||
emits: ['update:value'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
CruResource,
|
|
||||||
Tabbed,
|
|
||||||
Tab,
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
NameNsDescription,
|
|
||||||
RadioGroup,
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView, FormValidation],
|
|
||||||
|
|
||||||
inheritAttrs: false,
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
this.allSecrets = await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
if (!this.value.spec) this.value.spec = {};
|
|
||||||
if (!this.value.spec.credentials) this.value.spec.credentials = {};
|
|
||||||
|
|
||||||
const initialMode = this.value.spec.credentials.name ? 'existing' : 'new';
|
|
||||||
|
|
||||||
return {
|
|
||||||
allSecrets: [],
|
|
||||||
authMode: initialMode,
|
|
||||||
newUsername: '',
|
|
||||||
newPassword: '',
|
|
||||||
newCaCert: '',
|
|
||||||
|
|
||||||
fvFormRuleSets: [
|
|
||||||
{ path: 'metadata.name', rules: ['nameRequired'] },
|
|
||||||
{ path: 'spec.endpoint', rules: ['endpointRequired'] },
|
|
||||||
{ path: 'spec.dc', rules: ['dcRequired'] },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
authModeOptions() {
|
|
||||||
return [
|
|
||||||
{ label: this.t('harvester.addons.vmImport.fields.createSecret'), value: 'new' },
|
|
||||||
{ label: this.t('harvester.addons.vmImport.fields.useSecret'), value: 'existing' }
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
secretOptions() {
|
|
||||||
const currentNamespace = this.value.metadata.namespace || 'default';
|
|
||||||
|
|
||||||
return this.allSecrets
|
|
||||||
.filter((s) => s.metadata.namespace === currentNamespace)
|
|
||||||
.map((s) => ({
|
|
||||||
label: s.nameDisplay,
|
|
||||||
value: s.metadata.name
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
// Define custom rules for the FormValidation mixin
|
|
||||||
fvExtraRules() {
|
|
||||||
return {
|
|
||||||
nameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.fields.name') }) : undefined,
|
|
||||||
endpointRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.vmware.fields.endpoint') }) : undefined,
|
|
||||||
dcRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.vmware.fields.datacenter') }) : undefined,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
isFormValid() {
|
|
||||||
if (!this.fvFormIsValid) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.authMode === 'new') {
|
|
||||||
if (!this.newUsername || !this.newPassword) return false;
|
|
||||||
} else {
|
|
||||||
if (!this.value.spec.credentials.name) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
usernameRule(val) {
|
|
||||||
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.username') }) : undefined;
|
|
||||||
},
|
|
||||||
passwordRule(val) {
|
|
||||||
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.password') }) : undefined;
|
|
||||||
},
|
|
||||||
secretRule(val) {
|
|
||||||
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.selectSecret') }) : undefined;
|
|
||||||
},
|
|
||||||
|
|
||||||
async saveSource(buttonCb) {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.authMode === 'new') {
|
|
||||||
const secretName = `${ this.value.metadata.name }-creds-${ randomStr(4).toLowerCase() }`;
|
|
||||||
const namespace = this.value.metadata.namespace || 'default';
|
|
||||||
|
|
||||||
// Create the model with the correct Schema ID (SECRET)
|
|
||||||
const newSecret = await this.$store.dispatch(`${ inStore }/create`, {
|
|
||||||
type: SECRET,
|
|
||||||
metadata: {
|
|
||||||
name: secretName,
|
|
||||||
namespace
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use '_type' to set the Kubernetes 'type' field.
|
|
||||||
newSecret['_type'] = 'Opaque';
|
|
||||||
|
|
||||||
// base64 encode the data
|
|
||||||
newSecret['data'] = {
|
|
||||||
username: btoa(this.newUsername),
|
|
||||||
password: btoa(this.newPassword),
|
|
||||||
// Only include CA cert if the user provided one
|
|
||||||
caCert: this.newCaCert ? btoa(this.newCaCert) : undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
await newSecret.save();
|
|
||||||
|
|
||||||
// Link the new secret to the Source
|
|
||||||
this.value.spec.credentials = {
|
|
||||||
name: secretName,
|
|
||||||
namespace
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.save(buttonCb);
|
|
||||||
} catch (err) {
|
|
||||||
this.errors = [err];
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CruResource
|
|
||||||
:done-route="doneRoute"
|
|
||||||
:resource="value"
|
|
||||||
:mode="mode"
|
|
||||||
:errors="errors"
|
|
||||||
:apply-hooks="applyHooks"
|
|
||||||
:validation-passed="isFormValid"
|
|
||||||
@finish="saveSource"
|
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
|
||||||
<NameNsDescription
|
|
||||||
:value="value"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="{ name: fvGetAndReportPathRules('metadata.name') }"
|
|
||||||
@update:value="$emit('update:value', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Tabbed
|
|
||||||
v-bind="$attrs"
|
|
||||||
class="mt-15"
|
|
||||||
:side-tabs="true"
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
name="basic"
|
|
||||||
:label="t('harvester.addons.vmImport.titles.basic')"
|
|
||||||
:weight="2"
|
|
||||||
>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="value.spec.endpoint"
|
|
||||||
:label="t('harvester.addons.vmImport.vmware.fields.endpoint')"
|
|
||||||
:placeholder="t('harvester.addons.vmImport.vmware.placeholders.endpoint')"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="fvGetAndReportPathRules('spec.endpoint')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="value.spec.dc"
|
|
||||||
:label="t('harvester.addons.vmImport.vmware.fields.datacenter')"
|
|
||||||
:placeholder="t('harvester.addons.vmImport.vmware.placeholders.datacenter')"
|
|
||||||
:tooltip="t('harvester.addons.vmImport.vmware.tooltips.datacenter')"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="fvGetAndReportPathRules('spec.dc')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
|
||||||
name="auth"
|
|
||||||
:label="t('harvester.addons.vmImport.titles.auth')"
|
|
||||||
:weight="1"
|
|
||||||
>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-12">
|
|
||||||
<RadioGroup
|
|
||||||
v-model:value="authMode"
|
|
||||||
name="authMode"
|
|
||||||
:options="authModeOptions"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="authMode === 'new'">
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="newUsername"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.username')"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="[usernameRule]"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="newPassword"
|
|
||||||
type="password"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.password')"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="[passwordRule]"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="newCaCert"
|
|
||||||
type="multiline"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.caCert')"
|
|
||||||
:placeholder="t('harvester.addons.vmImport.placeholders.caCert')"
|
|
||||||
:min-height="100"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-muted">
|
|
||||||
Note: A new Kubernetes Secret will be created to store these credentials.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="authMode === 'existing'">
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="value.spec.credentials.name"
|
|
||||||
:options="secretOptions"
|
|
||||||
:label="t('harvester.addons.vmImport.fields.selectSecret')"
|
|
||||||
:mode="mode"
|
|
||||||
:rules="[secretRule]"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
</Tabbed>
|
|
||||||
</CruResource>
|
|
||||||
</template>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<script>
|
|
||||||
import NetworkPolicy from '@shell/edit/networking.k8s.io.networkpolicy';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import { DOC } from '../config/doc-links';
|
|
||||||
import { docLink } from '../utils/feature-flags';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
NetworkPolicy,
|
|
||||||
Banner
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
networkPolicyDocLink() {
|
|
||||||
const version = this.$store.getters['harvester-common/getServerVersion']();
|
|
||||||
|
|
||||||
return docLink(DOC.NETWORK_POLICY, version);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="network-policy-edit">
|
|
||||||
<Banner color="info">
|
|
||||||
<t
|
|
||||||
k="harvester.networkPolicy.banner"
|
|
||||||
:url="networkPolicyDocLink"
|
|
||||||
:raw="true"
|
|
||||||
/>
|
|
||||||
</Banner>
|
|
||||||
<NetworkPolicy v-bind="$attrs" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.network-policy-edit {
|
|
||||||
flex: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { NORMAN } from '@shell/config/types';
|
import { NORMAN } from '@shell/config/types';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import { getHarvesterUser } from '../utils/auth';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
@@ -26,7 +25,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
const user = getHarvesterUser(this.$store.getters);
|
const user = this.$store.getters['auth/v3User'];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
harvesterSettings: [],
|
harvesterSettings: [],
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import harvesterStore from './store/harvester-store';
|
|||||||
import customValidators from './validators';
|
import customValidators from './validators';
|
||||||
import { PRODUCT_NAME } from './config/harvester';
|
import { PRODUCT_NAME } from './config/harvester';
|
||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent } from 'vue';
|
||||||
import './styles/vue-flow.scss';
|
|
||||||
|
|
||||||
// Init the package
|
// Init the package
|
||||||
export default function (plugin: IPlugin) {
|
export default function (plugin: IPlugin) {
|
||||||
|
|||||||
+17
-342
@@ -20,7 +20,6 @@ nav:
|
|||||||
Monitoring: Monitoring
|
Monitoring: Monitoring
|
||||||
Logging: Logging
|
Logging: Logging
|
||||||
'Monitoring and Logging': Monitoring and Logging
|
'Monitoring and Logging': Monitoring and Logging
|
||||||
vmimport: Virtual Machine Imports
|
|
||||||
|
|
||||||
resourceTable:
|
resourceTable:
|
||||||
groupBy:
|
groupBy:
|
||||||
@@ -123,24 +122,6 @@ harvester:
|
|||||||
namespace: Namespace
|
namespace: Namespace
|
||||||
message:
|
message:
|
||||||
success: 'Image { name } created successfully.'
|
success: 'Image { name } created successfully.'
|
||||||
storageMigration:
|
|
||||||
title: Storage Migration
|
|
||||||
fields:
|
|
||||||
sourceVolume:
|
|
||||||
label: Source Volume
|
|
||||||
placeholder: Select a source volume
|
|
||||||
targetVolume:
|
|
||||||
label: Target Volume
|
|
||||||
placeholder: Select a target volume
|
|
||||||
dataMigration:
|
|
||||||
title: Data Migration
|
|
||||||
fields:
|
|
||||||
targetVolumeName:
|
|
||||||
label: Target Volume Name
|
|
||||||
placeholder: Enter a target volume name
|
|
||||||
targetStorageClassName:
|
|
||||||
label: Target Storage Class
|
|
||||||
placeholder: Select a storage class
|
|
||||||
migration:
|
migration:
|
||||||
failedMessage: Latest migration failed!
|
failedMessage: Latest migration failed!
|
||||||
title: Migration
|
title: Migration
|
||||||
@@ -173,24 +154,15 @@ 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.
|
||||||
hotplugVolume:
|
hotplug:
|
||||||
success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
|
success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
|
||||||
title: Add Volume
|
title: Add Volume
|
||||||
hotplugNic:
|
|
||||||
success: 'The settings have been saved, but the network interface {interfaceName} will be attached only after the virtual machine is migrated.'
|
|
||||||
title: Add Network Interface
|
|
||||||
vmNetwork: Virtual Machine Network
|
|
||||||
macAddress: MAC Address
|
|
||||||
macAddressTooltip: If left blank, the MAC address will be automatically generated.
|
|
||||||
insertCdRomVolume:
|
|
||||||
success: '{ imageName } is inserted into device { deviceName }.'
|
|
||||||
title: Insert Image
|
|
||||||
image: Image
|
|
||||||
actionLabel: Insert Image
|
|
||||||
cpuMemoryHotplug:
|
cpuMemoryHotplug:
|
||||||
success: 'CPU and Memory are updated to the virtual machine { vm }.'
|
success: 'CPU and Memory are updated to the virtual machine { vm }.'
|
||||||
title: Edit CPU and Memory
|
title: Edit CPU and Memory
|
||||||
maxResourcesMessage: 'You can increase the CPU to maximum { maxCpu }C and memory to maximum { maxMemory }.'
|
maxResourcesMessage: 'You can increase the CPU to maximum { maxCpu }C and memory to maximum { maxMemory }.'
|
||||||
|
hotunplug:
|
||||||
|
success: 'Volume { name } is detached successfully.'
|
||||||
snapshot:
|
snapshot:
|
||||||
title: Take Snapshot
|
title: Take Snapshot
|
||||||
name: Name
|
name: Name
|
||||||
@@ -219,9 +191,6 @@ harvester:
|
|||||||
info: Info
|
info: Info
|
||||||
warning: Warning
|
warning: Warning
|
||||||
error: Error
|
error: Error
|
||||||
restartRequired:
|
|
||||||
title: '{count} {count, plural, =1 {Virtual Machine is} other {Virtual Machines are}} Pending Restart'
|
|
||||||
message: 'Please restart { vmNames } to apply updated configurations'
|
|
||||||
action:
|
action:
|
||||||
createVM: Create Virtual Machine
|
createVM: Create Virtual Machine
|
||||||
start: Start
|
start: Start
|
||||||
@@ -250,9 +219,6 @@ harvester:
|
|||||||
migrate: Migrate
|
migrate: Migrate
|
||||||
cpuAndMemoryHotplug: Edit CPU and Memory
|
cpuAndMemoryHotplug: Edit CPU and Memory
|
||||||
abortMigration: Abort Migration
|
abortMigration: Abort Migration
|
||||||
storageMigration: Storage Migration
|
|
||||||
cancelStorageMigration: Cancel Storage Migration
|
|
||||||
dataMigration: Data Migration
|
|
||||||
createTemplate: Generate Template
|
createTemplate: Generate Template
|
||||||
enableMaintenance: Enable Maintenance Mode
|
enableMaintenance: Enable Maintenance Mode
|
||||||
disableMaintenance: Disable Maintenance Mode
|
disableMaintenance: Disable Maintenance Mode
|
||||||
@@ -260,8 +226,7 @@ harvester:
|
|||||||
disableCPUManager: Disable CPU Manager
|
disableCPUManager: Disable CPU Manager
|
||||||
cordon: Cordon
|
cordon: Cordon
|
||||||
uncordon: Uncordon
|
uncordon: Uncordon
|
||||||
addHotplugVolume: Add Volume
|
addHotplug: Add Volume
|
||||||
addHotplugNic: Hotplug Network Interface
|
|
||||||
exportImage: Export Image
|
exportImage: Export Image
|
||||||
viewlogs: View Logs
|
viewlogs: View Logs
|
||||||
cancelExpand: Cancel Expand
|
cancelExpand: Cancel Expand
|
||||||
@@ -271,7 +236,6 @@ 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:
|
||||||
@@ -307,7 +271,6 @@ 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
|
||||||
@@ -325,17 +288,6 @@ harvester:
|
|||||||
totalSnapshotQuota: Total Snapshot Quota
|
totalSnapshotQuota: Total Snapshot Quota
|
||||||
storageClass: Storage Class
|
storageClass: Storage Class
|
||||||
restore: Restore
|
restore: Restore
|
||||||
vmImportSourceVm: Source VM
|
|
||||||
vmImportSourceCluster: Source Cluster
|
|
||||||
vmImportStatus: Import Status
|
|
||||||
vmImportSourceVDatacenter: Datacenter
|
|
||||||
vmImportSourceVEndpoint: Endpoint
|
|
||||||
vmImportSourceVClusterStatus: Cluster Status
|
|
||||||
vmImportSourceORegion: Region
|
|
||||||
vmImportSourceOEndpoint: Endpoint
|
|
||||||
vmImportSourceOClusterStatus: Cluster Status
|
|
||||||
vmImportSourceOVAUrl: URL
|
|
||||||
vmImportSourceOVAStatus: Status
|
|
||||||
tab:
|
tab:
|
||||||
volume: Volumes
|
volume: Volumes
|
||||||
network: Networks
|
network: Networks
|
||||||
@@ -376,14 +328,7 @@ harvester:
|
|||||||
available: Available Devices
|
available: Available Devices
|
||||||
compatibleNodes: Compatible Nodes
|
compatibleNodes: Compatible Nodes
|
||||||
impossibleSelection: 'There are no hosts with all of the selected devices.'
|
impossibleSelection: 'There are no hosts with all of the selected devices.'
|
||||||
howToUseDeviceInVMCreation:
|
howToUseDevice: 'Use the table below to enable PCI passthrough on each device you want to use in this virtual machine.'
|
||||||
prefix: 'Use the table below to enable PCI passthrough on each device you want to use in this virtual machine. <br>For vGPU devices, please enable them on the'
|
|
||||||
middle: vGPU Devices
|
|
||||||
suffix: page first.
|
|
||||||
howToUseDevice:
|
|
||||||
prefix: 'Select the device in the table to enable PCI passthrough. <br>For vGPU devices, please enable them on the'
|
|
||||||
middle: vGPU Devices
|
|
||||||
suffix: page.
|
|
||||||
deviceInTheSameHost: 'You can only select devices on the same host.'
|
deviceInTheSameHost: 'You can only select devices on the same host.'
|
||||||
oldFormatDevices:
|
oldFormatDevices:
|
||||||
help: |-
|
help: |-
|
||||||
@@ -405,9 +350,6 @@ harvester:
|
|||||||
claimError: Error enabling passthrough on {name}
|
claimError: Error enabling passthrough on {name}
|
||||||
unclaimError: Error disabling passthrough on {name}
|
unclaimError: Error disabling passthrough on {name}
|
||||||
cantUnclaim: You cannot disable passthrough on a device claimed by another user.
|
cantUnclaim: You cannot disable passthrough on a device claimed by another user.
|
||||||
detachWarning:
|
|
||||||
title: Cannot Disable Passthrough
|
|
||||||
message: Please detach the device from the VM and save it first before disabling passthrough.
|
|
||||||
enableGroup: Enable Group
|
enableGroup: Enable Group
|
||||||
disableGroup: Disable Group
|
disableGroup: Disable Group
|
||||||
labelRequired: "This rule should not be manually altered: it ensures that the PCI devices selected for this virtual machine are available on the virtual machine's host."
|
labelRequired: "This rule should not be manually altered: it ensures that the PCI devices selected for this virtual machine are available on the virtual machine's host."
|
||||||
@@ -453,7 +395,7 @@ harvester:
|
|||||||
volume:
|
volume:
|
||||||
upperType: Volume name
|
upperType: Volume name
|
||||||
lowerType: volume name
|
lowerType: volume name
|
||||||
needAtLeastOneBootable: 'At least one bootable volume is required!'
|
needImageOrExisting: 'At least an image volume or an existing root-disk volume is required!'
|
||||||
image:
|
image:
|
||||||
ruleTip: 'The URL you have entered ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
ruleTip: 'The URL you have entered ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
||||||
ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
||||||
@@ -461,7 +403,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}"
|
||||||
@@ -498,7 +440,6 @@ 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:
|
||||||
@@ -571,20 +512,6 @@ 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:
|
||||||
@@ -649,28 +576,10 @@ harvester:
|
|||||||
virtualMachine:
|
virtualMachine:
|
||||||
label: Virtual Machines
|
label: Virtual Machines
|
||||||
osType: OS Type
|
osType: OS Type
|
||||||
cpuModel:
|
|
||||||
label: CPU Model
|
|
||||||
fetchError: 'Failed to load CPU model configuration: {error}'
|
|
||||||
optionLabel: "{modelName} ({count} {count, plural, one {node} other {nodes}})"
|
|
||||||
hotplug:
|
hotplug:
|
||||||
title: Enable CPU and memory hotplug
|
title: Enable CPU and memory hotplug
|
||||||
tooltip: The default maximum CPU and maximum memory are {hotPlugTimes} times based on CPU and memory.
|
tooltip: The default maximum CPU and maximum memory are {hotPlugTimes} times based on CPU and memory.
|
||||||
restartVMMessage: Restart action is required for the virtual machine configuration change to take effect
|
restartVMMessage: Restart action is required for the virtual machine configuration change to take effect
|
||||||
hotUnplug:
|
|
||||||
actionLabel: Detach
|
|
||||||
detachVolume:
|
|
||||||
title: 'Are you sure that you want to detach volume {name}?'
|
|
||||||
actionLabel: Detach Volume
|
|
||||||
success: 'Volume { name } is detached successfully.'
|
|
||||||
ejectCdRomVolume:
|
|
||||||
title: 'Are you sure that you want to eject image from device {name}?'
|
|
||||||
actionLabel: Eject Image
|
|
||||||
success: 'Image from device { name } is ejected successfully.'
|
|
||||||
detachNIC:
|
|
||||||
title: 'Are you sure that you want to detach network interface {name}?'
|
|
||||||
actionLabel: Detach Network Interface
|
|
||||||
success: 'The settings have been saved, but the network interface {name} will be detached only after the virtual machine is migrated.'
|
|
||||||
instance:
|
instance:
|
||||||
singleInstance:
|
singleInstance:
|
||||||
multipleInstance:
|
multipleInstance:
|
||||||
@@ -702,6 +611,11 @@ 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}
|
||||||
@@ -773,7 +687,6 @@ harvester:
|
|||||||
unmount:
|
unmount:
|
||||||
title: Are you sure?
|
title: Are you sure?
|
||||||
message: Are you sure you want to unmount this volume?
|
message: Are you sure you want to unmount this volume?
|
||||||
emptyImage: No media
|
|
||||||
network:
|
network:
|
||||||
title: Network
|
title: Network
|
||||||
addNetwork: Add Network
|
addNetwork: Add Network
|
||||||
@@ -923,14 +836,8 @@ 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
|
||||||
accessModes: Access Modes
|
|
||||||
createWithDataVolume: Create with DataVolume
|
|
||||||
createWithDataVolumeTooltip: Create Volume with Kubevirt/Containerized Data Importer way. It can fill accessMode/volumeMode automatically.
|
|
||||||
showAdvanced: Show Advanced Options
|
|
||||||
hideAdvanced: Hide Advanced Options
|
|
||||||
source: Source
|
source: Source
|
||||||
kind: Kind
|
kind: Kind
|
||||||
sourceOptions:
|
sourceOptions:
|
||||||
@@ -987,8 +894,6 @@ 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
|
||||||
@@ -1050,9 +955,7 @@ harvester:
|
|||||||
createTitle: Create Schedule
|
createTitle: Create Schedule
|
||||||
createButtonText: Create Schedule
|
createButtonText: Create Schedule
|
||||||
scheduleType: Virtual Machine Schedule Type
|
scheduleType: Virtual Machine Schedule Type
|
||||||
cron:
|
cron: Cron Schedule
|
||||||
label: Cron Schedule
|
|
||||||
editButton: Edit
|
|
||||||
detail:
|
detail:
|
||||||
namespace: Namespace
|
namespace: Namespace
|
||||||
sourceVM: Source Virtual Machine
|
sourceVM: Source Virtual Machine
|
||||||
@@ -1141,13 +1044,6 @@ 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.
|
||||||
@@ -1158,46 +1054,8 @@ harvester:
|
|||||||
placeholder: e.g. 172.16.0.0/16
|
placeholder: e.g. 172.16.0.0/16
|
||||||
excludeIPs:
|
excludeIPs:
|
||||||
tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list.
|
tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list.
|
||||||
placeholder: single IP or 192.168.0.100..192.168.0.200
|
|
||||||
|
|
||||||
acl:
|
|
||||||
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:
|
||||||
viewTopology: Topology
|
|
||||||
topology:
|
|
||||||
loading: Loading topology...
|
|
||||||
empty: No resources found
|
|
||||||
visibility:
|
|
||||||
vpc: VPC
|
|
||||||
subnets: Subnets
|
|
||||||
overlayNetworks: Overlay Networks
|
|
||||||
vms: VMs
|
|
||||||
labels:
|
|
||||||
cidr: CIDR
|
|
||||||
provider: Provider
|
|
||||||
type: Type
|
|
||||||
clusterNetwork: Cluster Network
|
|
||||||
network: Network
|
|
||||||
subnet: Subnet
|
|
||||||
ip: IP
|
|
||||||
mac: MAC
|
|
||||||
peering: Peering
|
|
||||||
noAddonEnabled:
|
noAddonEnabled:
|
||||||
prefix: The kubeovn-operator add-on is not enabled, click
|
prefix: The kubeovn-operator add-on is not enabled, click
|
||||||
middle: here
|
middle: here
|
||||||
@@ -1222,10 +1080,6 @@ 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:
|
||||||
@@ -1294,19 +1148,13 @@ harvester:
|
|||||||
vlan: VLAN ID
|
vlan: VLAN ID
|
||||||
exclude:
|
exclude:
|
||||||
label: Exclude IPs
|
label: Exclude IPs
|
||||||
placeholder: CIDR format, e.g. 172.16.0.10/32
|
placeholder: e.g. 172.16.0.1
|
||||||
invalid: '"Exclude list" is invalid.'
|
invalid: '"Exclude list" is invalid.'
|
||||||
addIp: Add Exclude IP
|
addIp: Add Exclude IP
|
||||||
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
||||||
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
|
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
|
||||||
rwxNetwork:
|
|
||||||
warning: 'WARNING: <br/> Any change to rwx-network requires longhorn RWX volumes detached before applying this setting.<br/>Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
|
||||||
shareStorageNetwork: Share Storage Network
|
|
||||||
dedicatedRwxNetwork: Dedicated RWX Network
|
|
||||||
shareStorageNetworkWarning: The rwx-network is governed by storage-network, and changes here won't take effect until share-storage-network is set to false.
|
|
||||||
vmForceDeletionPolicy:
|
vmForceDeletionPolicy:
|
||||||
period: Period
|
period: Period
|
||||||
vmMigrationTimeout: VM Migration Timeout
|
|
||||||
vmMigrationNetwork:
|
vmMigrationNetwork:
|
||||||
parseError: "Failed to parse existing configuration."
|
parseError: "Failed to parse existing configuration."
|
||||||
fetchError: "Failed to load required network resources: {error}. Please refresh the page or try again later."
|
fetchError: "Failed to load required network resources: {error}. Please refresh the page or try again later."
|
||||||
@@ -1370,10 +1218,7 @@ harvester:
|
|||||||
deleteImage: Please select an image to delete.
|
deleteImage: Please select an image to delete.
|
||||||
deleteSuccess: "{name} deleted successfully."
|
deleteSuccess: "{name} deleted successfully."
|
||||||
imagePreloadStrategy: Image Preload Strategy
|
imagePreloadStrategy: Image Preload Strategy
|
||||||
nodeUpgradeOption: Node Upgrade Option
|
|
||||||
restoreVM: Restore VM
|
restoreVM: Restore VM
|
||||||
strategy: Strategy
|
|
||||||
pauseNodes: Pause Nodes
|
|
||||||
strategyType: Strategy Type
|
strategyType: Strategy Type
|
||||||
concurrency: Concurrency
|
concurrency: Concurrency
|
||||||
harvesterMonitoring:
|
harvesterMonitoring:
|
||||||
@@ -1389,35 +1234,10 @@ 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.
|
||||||
instanceManagerResources:
|
|
||||||
parseError: "Failed to parse configuration: {error}"
|
|
||||||
v1: "V1 Data Engine"
|
|
||||||
v2: "V2 Data Engine"
|
|
||||||
kubevirtMigration:
|
|
||||||
parseError: "Failed to parse configuration: {error}"
|
|
||||||
parallelMigrationsPerCluster: "Parallel Migrations Per Cluster"
|
|
||||||
parallelOutboundMigrationsPerNode: "Parallel Outbound Migrations Per Node"
|
|
||||||
bandwidthPerMigration: "Bandwidth Per Migration"
|
|
||||||
bandwidthPerMigrationTooltip: '0 is KubeVirt default, representing unlimited bandwidth'
|
|
||||||
completionTimeoutPerGiB: "Completion Timeout Per GiB"
|
|
||||||
progressTimeout: "Progress Timeout"
|
|
||||||
allowAutoConverge: "Allow Auto Converge"
|
|
||||||
allowPostCopy: "Allow Post Copy"
|
|
||||||
unsafeMigrationOverride: "Unsafe Migration Override"
|
|
||||||
allowWorkloadDisruption: "Allow Workload Disruption"
|
|
||||||
disableTLS: "Disable TLS"
|
|
||||||
matchSELinuxLevelOnMigration: "Match SELinux Level On Migration"
|
|
||||||
|
|
||||||
cloudTemplate:
|
cloudTemplate:
|
||||||
label: Cloud Configuration Templates
|
label: Cloud Configuration Templates
|
||||||
templateType: Template Type
|
templateType: Template Type
|
||||||
@@ -1633,14 +1453,6 @@ 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
|
||||||
@@ -1668,86 +1480,10 @@ harvester:
|
|||||||
'harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
|
'harvester-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
|
||||||
auth: Authentication
|
|
||||||
pvc: Volume
|
pvc: Volume
|
||||||
networking: Network Mapping
|
|
||||||
advanced: Advanced
|
|
||||||
labels:
|
|
||||||
vmimport: Virtual Machine Import
|
|
||||||
vmimportSourceVMWare: Source VMWare
|
|
||||||
vmimportSourceOpenStack: Source OpenStack
|
|
||||||
vmimportSourceOVA: Source OVA
|
|
||||||
fields:
|
|
||||||
sourceProvider: Source Provider Type
|
|
||||||
sourceCluster: Source Cluster
|
|
||||||
vmName: VM Name
|
|
||||||
targetStorageClass: Target Storage Class
|
|
||||||
sourceNetwork: Source Network Name
|
|
||||||
destNetwork: Destination Network
|
|
||||||
interfaceModel: Interface Model
|
|
||||||
folder: Folder
|
|
||||||
diskBus: Default Disk Bus
|
|
||||||
defaultInterface: Default Network Interface
|
|
||||||
skipPreflight: Skip Preflight Checks
|
|
||||||
forcePowerOff: Force Power Off Source VM
|
|
||||||
username: Username
|
|
||||||
password: Password
|
|
||||||
caCert: CA Certificate (PEM)
|
|
||||||
selectSecret: Select Secret
|
|
||||||
createSecret: Create New Credentials
|
|
||||||
useSecret: Use Existing Secret
|
|
||||||
none: None (Public URL)
|
|
||||||
placeholders:
|
|
||||||
selectCluster: Select a cluster...
|
|
||||||
selectProviderFirst: Select a provider type first
|
|
||||||
matchSource: Must match the name in the source cluster
|
|
||||||
folderExample: e.g. /Datacenters/DC1/vm
|
|
||||||
caCert: "-----BEGIN CERTIFICATE----- ..."
|
|
||||||
options:
|
|
||||||
useDefault: Use Default
|
|
||||||
actions:
|
|
||||||
addNetwork: Add Network Mapping
|
|
||||||
remove: Remove
|
|
||||||
errors:
|
|
||||||
rfc1123: 'Invalid format. Name must be lowercase, alphanumeric, and cannot contain spaces (e.g. "my-vm-1"). If your Source VM name does not match this, you must rename it on the Source cluster first.'
|
|
||||||
networkMappingRequired: Every Network Mapping row must have a Source and Destination selected.
|
|
||||||
openstack:
|
|
||||||
fields:
|
|
||||||
endpoint: Identity Service Endpoint
|
|
||||||
region: Region
|
|
||||||
projectName: Project Name
|
|
||||||
domainName: Domain Name
|
|
||||||
retryCount: Upload Image Retry Count
|
|
||||||
retryDelay: Upload Image Retry Delay
|
|
||||||
placeholders:
|
|
||||||
endpoint: "e.g. https://devstack/identity"
|
|
||||||
region: e.g. RegionOne
|
|
||||||
projectName: e.g. admin
|
|
||||||
domainName: e.g. default
|
|
||||||
retryCount: "Default: 30"
|
|
||||||
retryDelay: "Default: 10"
|
|
||||||
vmware:
|
|
||||||
fields:
|
|
||||||
endpoint: vCenter Endpoint
|
|
||||||
datacenter: Datacenter
|
|
||||||
placeholders:
|
|
||||||
endpoint: "e.g. https://vscim/sdk"
|
|
||||||
datacenter: e.g. DC0
|
|
||||||
tooltips:
|
|
||||||
datacenter: The exact name of the Datacenter object in vCenter
|
|
||||||
ova:
|
|
||||||
fields:
|
|
||||||
url: URL
|
|
||||||
httpTimeout: HTTP Timeout
|
|
||||||
placeholders:
|
|
||||||
url: "e.g. https://download.example.com/images/my-vm.ova"
|
|
||||||
httpTimeout: "Default: 600"
|
|
||||||
|
|
||||||
rancherVcluster:
|
rancherVcluster:
|
||||||
accessRancher: Access the Rancher Dashboard
|
accessRancher: Access the Rancher Dashboard
|
||||||
hostname: Hostname
|
hostname: Hostname
|
||||||
@@ -1874,8 +1610,7 @@ harvester:
|
|||||||
numVFs: Number Of Virtual Functions
|
numVFs: Number Of Virtual Functions
|
||||||
vfAddresses: Virtual Functions Addresses
|
vfAddresses: Virtual Functions Addresses
|
||||||
showMore: Show More
|
showMore: Show More
|
||||||
parentSriov: Filter By Parent SR-IOV Netork Device
|
parentSriov: Filter By Parent SR-IOV
|
||||||
parentSriovGPU: Filter By Parent SR-IOV GPU Device
|
|
||||||
|
|
||||||
sriovgpu:
|
sriovgpu:
|
||||||
label: SR-IOV GPU Devices
|
label: SR-IOV GPU Devices
|
||||||
@@ -1891,32 +1626,9 @@ 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.
|
||||||
detachWarning:
|
|
||||||
title: Cannot Disable vGPU
|
|
||||||
message: Please detach the device from the VM and save it first before disabling this vGPU device.
|
|
||||||
goSetting:
|
goSetting:
|
||||||
prefix: The nvidia-driver-toolkit add-on is not enabled, click
|
prefix: The nvidia-driver-toolkit add-on is not enabled, click
|
||||||
middle: here
|
middle: here
|
||||||
@@ -1951,9 +1663,6 @@ harvester:
|
|||||||
claimError: Error enabling passthrough on {name}
|
claimError: Error enabling passthrough on {name}
|
||||||
unclaimError: Error disabling passthrough on {name}
|
unclaimError: Error disabling passthrough on {name}
|
||||||
cantUnclaim: You cannot disable passthrough on a device claimed by another user.
|
cantUnclaim: You cannot disable passthrough on a device claimed by another user.
|
||||||
detachWarning:
|
|
||||||
title: Cannot Disable Passthrough
|
|
||||||
message: Please detach the device from the VM and save it first before disabling passthrough.
|
|
||||||
enablePassthroughWarning: 'Please re-enable the USB device if the device path changes in the following situations:<br/> 1) Re-plugging the USB device.<br/> 2) Rebooting the node.<br/><br/>An incorrect device path may cause passthrough to fail.'
|
enablePassthroughWarning: 'Please re-enable the USB device if the device path changes in the following situations:<br/> 1) Re-plugging the USB device.<br/> 2) Rebooting the node.<br/><br/>An incorrect device path may cause passthrough to fail.'
|
||||||
|
|
||||||
harvesterVlanConfigMigrateDialog:
|
harvesterVlanConfigMigrateDialog:
|
||||||
@@ -2009,20 +1718,19 @@ 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. The `External` option is used to load the UI from a ui-index URL, while the `Bundled` option loads the UI from the local Harvester server.
|
'harv-ui-source': Configure how to load the UI source.
|
||||||
'harv-ui-index': 'HTML index location for the UI.'
|
'harv-ui-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.'
|
||||||
'harv-overcommit-config': 'Resource overcommit configuration.'
|
'harv-overcommit-config': 'Resource overcommit configuration.'
|
||||||
'harv-support-bundle-timeout': 'Support bundle timeout configuration in minutes, use 0 to disable the timeout.'
|
'harv-support-bundle-timeout': 'Support bundle timeout configuration in minutes, use 0 to disable the timeout.'
|
||||||
'harv-support-bundle-file-name': 'Support bundle file name configuration.'
|
|
||||||
'harv-support-bundle-expiration': 'Support bundle expiration configuration in minutes.'
|
'harv-support-bundle-expiration': 'Support bundle expiration configuration in minutes.'
|
||||||
'harv-support-bundle-node-collection-timeout': 'Support bundle node collection timeout configuration in minutes.'
|
'harv-support-bundle-node-collection-timeout': 'Support bundle node collection timeout configuration in minutes.'
|
||||||
'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down.
|
'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down.
|
||||||
'harv-ssl-parameters': Custom SSL Parameters for TLS validation.
|
'harv-ssl-parameters': Custom SSL Parameters for TLS validation.
|
||||||
'harv-storage-network': 'Longhorn storage-network setting.'
|
'harv-storage-network': 'Longhorn storage-network setting.'
|
||||||
'harv-rwx-network': 'Configure RWX network behavior for shared or dedicated storage network usage.'
|
|
||||||
'harv-support-bundle-namespaces': Select additional namespaces to include in the support bundle.
|
'harv-support-bundle-namespaces': Select additional namespaces to include in the support bundle.
|
||||||
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage.
|
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage.
|
||||||
'harv-support-bundle-image': Support bundle image configuration. Find different versions in <a href="https://hub.docker.com/r/rancher/support-bundle-kit/tags" target="_blank">rancher/support-bundle-kit</a>.
|
'harv-support-bundle-image': Support bundle image configuration. Find different versions in <a href="https://hub.docker.com/r/rancher/support-bundle-kit/tags" target="_blank">rancher/support-bundle-kit</a>.
|
||||||
@@ -2037,8 +1745,6 @@ advancedSettings:
|
|||||||
'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-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-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
|
||||||
'harv-kubevirt-migration': 'Configure cluster-wide KubeVirt live migration parameters.'
|
|
||||||
'harv-instance-manager-resources': 'Configure resource percentage reservations for Longhorn instance manager V1 and V2. Valid instance manager CPU range between 0 - 40.'
|
|
||||||
|
|
||||||
typeLabel:
|
typeLabel:
|
||||||
kubevirt.io.virtualmachine: |-
|
kubevirt.io.virtualmachine: |-
|
||||||
@@ -2101,11 +1807,6 @@ 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 }
|
||||||
@@ -2194,12 +1895,6 @@ 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 }
|
||||||
@@ -2220,23 +1915,3 @@ typeLabel:
|
|||||||
one { IP Pool }
|
one { IP Pool }
|
||||||
other { IP Pools }
|
other { IP Pools }
|
||||||
}
|
}
|
||||||
migration.harvesterhci.io.openstacksource: |-
|
|
||||||
{count, plural,
|
|
||||||
one { OpenStack Source }
|
|
||||||
other { OpenStack Sources }
|
|
||||||
}
|
|
||||||
migration.harvesterhci.io.vmwaresource: |-
|
|
||||||
{count, plural,
|
|
||||||
one { VMware Source }
|
|
||||||
other { VMware Sources }
|
|
||||||
}
|
|
||||||
migration.harvesterhci.io.ovasource: |-
|
|
||||||
{count, plural,
|
|
||||||
one { OVA Source }
|
|
||||||
other { OVA Sources }
|
|
||||||
}
|
|
||||||
migration.harvesterhci.io.virtualmachineimport: |-
|
|
||||||
{count, plural,
|
|
||||||
one { Virtual Machine Import }
|
|
||||||
other { Virtual Machine Imports }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { STATE, NAME } from '@shell/config/table-headers';
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
|
||||||
import Banner from '@components/Banner/Banner.vue';
|
|
||||||
import Loading from '@shell/components/Loading';
|
|
||||||
import ResourceTable from '@shell/components/ResourceTable';
|
|
||||||
import { HCI } from '../types';
|
|
||||||
import { ADD_ONS } from '../config/harvester-map';
|
|
||||||
import MessageLink from '@shell/components/MessageLink';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'ListMIGConfigurations',
|
|
||||||
|
|
||||||
inheritAttrs: false,
|
|
||||||
|
|
||||||
components: {
|
|
||||||
Banner,
|
|
||||||
Loading,
|
|
||||||
ResourceTable,
|
|
||||||
MessageLink,
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
this.schema = this.$store.getters[`${ inStore }/schemaFor`](HCI.MIG_CONFIGURATION);
|
|
||||||
this.hasAddonSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.ADD_ONS);
|
|
||||||
|
|
||||||
if (this.hasSchema) {
|
|
||||||
try {
|
|
||||||
const hash = await allHash({
|
|
||||||
migconfigs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.MIG_CONFIGURATION }),
|
|
||||||
vGpuDevices: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VGPU_DEVICE }),
|
|
||||||
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS })
|
|
||||||
});
|
|
||||||
|
|
||||||
this.hasPCIAddon = hash.addons.find((addon) => addon.name === ADD_ONS.PCI_DEVICE_CONTROLLER)?.spec?.enabled === true;
|
|
||||||
this.hasSriovgpuAddon = hash.addons.find((addon) => addon.name === ADD_ONS.NVIDIA_DRIVER_TOOLKIT_CONTROLLER)?.spec?.enabled === true;
|
|
||||||
this.hasSRIOVGPUSchema = !!this.$store.getters[`${ inStore }/schemaFor`](HCI.SR_IOVGPU_DEVICE);
|
|
||||||
|
|
||||||
if (this.hasSRIOVGPUSchema) {
|
|
||||||
await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOVGPU_DEVICE });
|
|
||||||
}
|
|
||||||
this.rows = hash.migconfigs;
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
rows: [],
|
|
||||||
schema: null,
|
|
||||||
hasAddonSchema: false,
|
|
||||||
hasPCIAddon: false,
|
|
||||||
hasSriovgpuAddon: false,
|
|
||||||
hasSRIOVGPUSchema: false,
|
|
||||||
toVGpuAddon: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.NVIDIA_DRIVER_TOOLKIT_CONTROLLER }?mode=edit`,
|
|
||||||
toPciAddon: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.PCI_DEVICE_CONTROLLER }?mode=edit`,
|
|
||||||
SRIOVGPUPage: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.NVIDIA_DRIVER_TOOLKIT_CONTROLLER }?mode=edit`,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
hasSchema() {
|
|
||||||
return !!this.schema;
|
|
||||||
},
|
|
||||||
|
|
||||||
rowsData() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const rows = this.$store.getters[`${ inStore }/all`](HCI.MIG_CONFIGURATION) || [];
|
|
||||||
|
|
||||||
return rows;
|
|
||||||
},
|
|
||||||
|
|
||||||
sriovGPUPage() {
|
|
||||||
return {
|
|
||||||
name: 'harvester-c-cluster-resource',
|
|
||||||
params: { cluster: this.$store.getters['clusterId'], resource: HCI.SR_IOVGPU_DEVICE },
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
showEnableSRIOVGPUMessage() {
|
|
||||||
return this.rowsData.length === 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
headers() {
|
|
||||||
const cols = [
|
|
||||||
STATE,
|
|
||||||
NAME,
|
|
||||||
{
|
|
||||||
name: 'address',
|
|
||||||
label: 'Address',
|
|
||||||
value: 'spec.gpuAddress',
|
|
||||||
sort: ['spec.gpuAddress']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Configured Profile',
|
|
||||||
label: 'Configured Count',
|
|
||||||
labelKey: 'harvester.tableHeaders.configuredProfiles',
|
|
||||||
value: 'configuredProfiles',
|
|
||||||
sort: ['configuredProfiles'],
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'status',
|
|
||||||
label: 'Status',
|
|
||||||
labelKey: 'tableHeaders.status',
|
|
||||||
sort: ['status.status'],
|
|
||||||
value: 'status.status',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return cols;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Loading v-if="$fetchState.pending" />
|
|
||||||
<div v-else-if="!hasAddonSchema">
|
|
||||||
<Banner color="warning">
|
|
||||||
{{ t('harvester.vgpu.noPermission') }}
|
|
||||||
</Banner>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="!hasSriovgpuAddon || !hasPCIAddon">
|
|
||||||
<Banner
|
|
||||||
v-if="!hasSriovgpuAddon"
|
|
||||||
color="warning"
|
|
||||||
>
|
|
||||||
<MessageLink
|
|
||||||
:to="toVGpuAddon"
|
|
||||||
prefix-label="harvester.vgpu.goSetting.prefix"
|
|
||||||
middle-label="harvester.vgpu.goSetting.middle"
|
|
||||||
suffix-label="harvester.vgpu.goSetting.suffix"
|
|
||||||
/>
|
|
||||||
</Banner>
|
|
||||||
<Banner
|
|
||||||
v-if="!hasPCIAddon"
|
|
||||||
color="warning"
|
|
||||||
>
|
|
||||||
<MessageLink
|
|
||||||
:to="toPciAddon"
|
|
||||||
prefix-label="harvester.pci.goSetting.prefix"
|
|
||||||
middle-label="harvester.pci.goSetting.middle"
|
|
||||||
suffix-label="harvester.pci.goSetting.suffix"
|
|
||||||
/>
|
|
||||||
</Banner>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="hasSchema">
|
|
||||||
<Banner
|
|
||||||
v-if="showEnableSRIOVGPUMessage"
|
|
||||||
color="warning"
|
|
||||||
>
|
|
||||||
<MessageLink
|
|
||||||
:to="sriovGPUPage"
|
|
||||||
prefix-label="harvester.migconfiguration.goSriovGPU.prefix"
|
|
||||||
middle-label="harvester.migconfiguration.goSriovGPU.middle"
|
|
||||||
suffix-label="harvester.migconfiguration.goSriovGPU.suffix"
|
|
||||||
/>
|
|
||||||
</Banner>
|
|
||||||
<Banner
|
|
||||||
v-if="!showEnableSRIOVGPUMessage"
|
|
||||||
color="warning"
|
|
||||||
:label="t('harvester.migconfiguration.infoBanner')"
|
|
||||||
/>
|
|
||||||
<ResourceTable
|
|
||||||
v-bind="$attrs"
|
|
||||||
:groupable="false"
|
|
||||||
:namespaced="false"
|
|
||||||
:headers="headers"
|
|
||||||
:schema="schema"
|
|
||||||
:rows="rowsData"
|
|
||||||
key-field="_key"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -60,25 +60,10 @@ export default {
|
|||||||
return schema;
|
return schema;
|
||||||
},
|
},
|
||||||
|
|
||||||
toVGpuDevicesPage() {
|
|
||||||
return {
|
|
||||||
name: 'harvester-c-cluster-resource',
|
|
||||||
params: { cluster: this.$store.getters['clusterId'], resource: HCI.VGPU_DEVICE },
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
vGPUAsPCIDeviceEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
|
|
||||||
},
|
|
||||||
|
|
||||||
rows() {
|
rows() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
const rows = this.$store.getters[`${ inStore }/all`](HCI.PCI_DEVICE);
|
const rows = this.$store.getters[`${ inStore }/all`](HCI.PCI_DEVICE);
|
||||||
|
|
||||||
rows.forEach((row) => {
|
|
||||||
row.allowDisable = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -96,23 +81,11 @@ export default {
|
|||||||
{{ t('harvester.pci.noPCIPermission') }}
|
{{ t('harvester.pci.noPCIPermission') }}
|
||||||
</Banner>
|
</Banner>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="hasSchema && enabledPCI">
|
<DeviceList
|
||||||
<Banner
|
v-else-if="hasSchema && enabledPCI"
|
||||||
v-if="vGPUAsPCIDeviceEnabled"
|
:devices="rows"
|
||||||
color="info"
|
:schema="schema"
|
||||||
>
|
/>
|
||||||
<MessageLink
|
|
||||||
:to="toVGpuDevicesPage"
|
|
||||||
prefix-label="harvester.pci.howToUseDevice.prefix"
|
|
||||||
middle-label="harvester.pci.howToUseDevice.middle"
|
|
||||||
suffix-label="harvester.pci.howToUseDevice.suffix"
|
|
||||||
/>
|
|
||||||
</Banner>
|
|
||||||
<DeviceList
|
|
||||||
:devices="rows"
|
|
||||||
:schema="schema"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<Banner color="warning">
|
<Banner color="warning">
|
||||||
<MessageLink
|
<MessageLink
|
||||||
|
|||||||
@@ -54,13 +54,7 @@ export default {
|
|||||||
devices() {
|
devices() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
const data = this.$store.getters[`${ inStore }/all`](HCI.USB_DEVICE) || [];
|
return this.$store.getters[`${ inStore }/all`](HCI.USB_DEVICE) || [];
|
||||||
|
|
||||||
data.forEach((row) => {
|
|
||||||
row.allowDisable = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -65,10 +65,6 @@ export default {
|
|||||||
const vGpuDevices = this.$store.getters[`${ inStore }/all`](HCI.VGPU_DEVICE) || [];
|
const vGpuDevices = this.$store.getters[`${ inStore }/all`](HCI.VGPU_DEVICE) || [];
|
||||||
const srioVGpuDevices = this.$store.getters[`${ inStore }/all`](HCI.SR_IOVGPU_DEVICE) || [];
|
const srioVGpuDevices = this.$store.getters[`${ inStore }/all`](HCI.SR_IOVGPU_DEVICE) || [];
|
||||||
|
|
||||||
vGpuDevices.forEach((row) => {
|
|
||||||
row.allowDisable = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.hasSRIOVGPUSchema) {
|
if (this.hasSRIOVGPUSchema) {
|
||||||
return vGpuDevices.filter((device) => !!srioVGpuDevices.find((s) => s.isEnabled && s.spec?.nodeName === device.spec?.nodeName));
|
return vGpuDevices.filter((device) => !!srioVGpuDevices.find((s) => s.isEnabled && s.spec?.nodeName === device.spec?.nodeName));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Loading from '@shell/components/Loading';
|
|||||||
import MessageLink from '@shell/components/MessageLink';
|
import 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 = isBackupTargetSettingEmpty(backupTargetResource);
|
const isEmpty = this.getBackupTargetValueIsEmpty(backupTargetResource);
|
||||||
|
|
||||||
if (backupTargetResource && !isEmpty) {
|
if (backupTargetResource && !isEmpty) {
|
||||||
this.testConnect();
|
this.testConnect();
|
||||||
@@ -78,6 +78,20 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getBackupTargetValueIsEmpty(resource) {
|
||||||
|
let out = true;
|
||||||
|
|
||||||
|
if (resource?.value) {
|
||||||
|
try {
|
||||||
|
const valueJson = JSON.parse(resource?.value);
|
||||||
|
|
||||||
|
out = !valueJson.type;
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
getRow(row) {
|
getRow(row) {
|
||||||
return row.status && row.status.source;
|
return row.status && row.status.source;
|
||||||
},
|
},
|
||||||
@@ -169,9 +183,11 @@ 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 isBackupTargetSettingEmpty(this.backupTargetResource);
|
return this.getBackupTargetValueIsEmpty(this.backupTargetResource);
|
||||||
},
|
},
|
||||||
|
|
||||||
canUpdate() {
|
canUpdate() {
|
||||||
return this?.backupTargetResource?.canUpdate;
|
return this?.backupTargetResource?.canUpdate;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -70,13 +70,8 @@ export default {
|
|||||||
return schema;
|
return schema;
|
||||||
},
|
},
|
||||||
filterRows() {
|
filterRows() {
|
||||||
return this.rows.filter((pvc) => {
|
// we only show the non golden image PVCs in the list
|
||||||
if (pvc?.isGoldenImageVolume || pvc?.isCDIPopulatorVolume) {
|
return this.rows.filter((pvc) => !pvc?.isGoldenImageVolume);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
headers() {
|
headers() {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -155,15 +155,6 @@ export default {
|
|||||||
return location;
|
return location;
|
||||||
},
|
},
|
||||||
|
|
||||||
viewTopology(group) {
|
|
||||||
const vpc = group.key;
|
|
||||||
const resource = this.$store.getters[`harvester/byId`](HCI.VPC, vpc);
|
|
||||||
|
|
||||||
if (resource && resource.goToDetail) {
|
|
||||||
resource.goToDetail();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
showVpcAction(event, group) {
|
showVpcAction(event, group) {
|
||||||
const vpc = group.key;
|
const vpc = group.key;
|
||||||
|
|
||||||
@@ -227,14 +218,6 @@ export default {
|
|||||||
>
|
>
|
||||||
{{ t('harvester.vpc.createSubnet') }}
|
{{ t('harvester.vpc.createSubnet') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm role-secondary mr-5"
|
|
||||||
@click="viewTopology(group)"
|
|
||||||
>
|
|
||||||
<i class="icon icon-globe mr-5" />
|
|
||||||
{{ t('harvester.vpc.viewTopology') }}
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm role-multi-action actions mr-10"
|
class="btn btn-sm role-multi-action actions mr-10"
|
||||||
|
|||||||
@@ -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, opt: { force: true } }),
|
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
|
||||||
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 }),
|
||||||
@@ -110,12 +110,11 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
hasNode: false,
|
hasNode: false,
|
||||||
allVMs: [],
|
allVMs: [],
|
||||||
allVMIs: [],
|
allVMIs: [],
|
||||||
allNodeNetworks: [],
|
allNodeNetworks: [],
|
||||||
allClusterNetworks: [],
|
allClusterNetworks: [],
|
||||||
restartNotificationDisplayed: false,
|
|
||||||
HCI
|
HCI
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -175,48 +174,6 @@ export default {
|
|||||||
this['allVMIs'] = vmis;
|
this['allVMIs'] = vmis;
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeUnmount() {
|
|
||||||
// clear restart message before component unmount
|
|
||||||
this.$store.dispatch('growl/clear');
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
allVMs: {
|
|
||||||
handler(neu) {
|
|
||||||
const vmNames = [];
|
|
||||||
|
|
||||||
neu.forEach((vm) => {
|
|
||||||
if (vm.isRestartRequired) {
|
|
||||||
vmNames.push(vm.metadata.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const count = vmNames.length;
|
|
||||||
|
|
||||||
if ( count === 0 && this.restartNotificationDisplayed) {
|
|
||||||
this.restartNotificationDisplayed = false;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count > 0) {
|
|
||||||
// clear old notification before showing new one
|
|
||||||
if (this.restartNotificationDisplayed) {
|
|
||||||
this.$store.dispatch('growl/clear');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count > 0 && vmNames.length > 0) {
|
|
||||||
this.$store.dispatch('growl/warning', {
|
|
||||||
title: this.t('harvester.notification.restartRequired.title', { count }),
|
|
||||||
message: this.t('harvester.notification.restartRequired.message', { vmNames: vmNames.join(', ') }),
|
|
||||||
timeout: 10000,
|
|
||||||
}, { root: true });
|
|
||||||
this.restartNotificationDisplayed = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deep: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
lockIconTooltipMessage(row) {
|
lockIconTooltipMessage(row) {
|
||||||
const message = '';
|
const message = '';
|
||||||
@@ -286,12 +243,6 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.growl-container {
|
|
||||||
z-index: 56 !important; // set to be lower than the vm action menu (z-index: 57)
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.state {
|
.state {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
<script>
|
|
||||||
import ResourceTable from '@shell/components/ResourceTable';
|
|
||||||
import Loading from '@shell/components/Loading';
|
|
||||||
import { SCHEMA } from '@shell/config/types';
|
|
||||||
import { HCI } from '../types';
|
|
||||||
|
|
||||||
const schema = {
|
|
||||||
id: HCI.VMIMPORT_SOURCE_O,
|
|
||||||
type: SCHEMA,
|
|
||||||
attributes: {
|
|
||||||
kind: HCI.VMIMPORT_SOURCE_O,
|
|
||||||
namespaced: true
|
|
||||||
},
|
|
||||||
metadata: { name: HCI.VMIMPORT_SOURCE_O },
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterVMImportSourceO',
|
|
||||||
components: { ResourceTable, Loading },
|
|
||||||
inheritAttrs: false,
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIMPORT_SOURCE_O });
|
|
||||||
|
|
||||||
const configSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.VMIMPORT_SOURCE_O);
|
|
||||||
|
|
||||||
if (!configSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) {
|
|
||||||
this.$store.dispatch('type-map/configureType', { match: HCI.VMIMPORT_SOURCE_O, isCreatable: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return { rows: [] };
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
schema() {
|
|
||||||
return schema;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
typeDisplay() {
|
|
||||||
return this.$store.getters['type-map/labelFor'](schema, 99);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Loading v-if="$fetchState.pending" />
|
|
||||||
<ResourceTable
|
|
||||||
v-else
|
|
||||||
v-bind="$attrs"
|
|
||||||
:groupable="true"
|
|
||||||
:schema="schema"
|
|
||||||
:rows="rows"
|
|
||||||
key-field="_key"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<script>
|
|
||||||
import ResourceTable from '@shell/components/ResourceTable';
|
|
||||||
import Loading from '@shell/components/Loading';
|
|
||||||
import { SCHEMA } from '@shell/config/types';
|
|
||||||
import { HCI } from '../types';
|
|
||||||
|
|
||||||
const schema = {
|
|
||||||
id: HCI.VMIMPORT_SOURCE_OVA,
|
|
||||||
type: SCHEMA,
|
|
||||||
attributes: {
|
|
||||||
kind: HCI.VMIMPORT_SOURCE_OVA,
|
|
||||||
namespaced: true
|
|
||||||
},
|
|
||||||
metadata: { name: HCI.VMIMPORT_SOURCE_OVA },
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterVMImportSourceOVA',
|
|
||||||
components: { ResourceTable, Loading },
|
|
||||||
inheritAttrs: false,
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIMPORT_SOURCE_OVA });
|
|
||||||
|
|
||||||
const configSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.VMIMPORT_SOURCE_OVA);
|
|
||||||
|
|
||||||
if (!configSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) {
|
|
||||||
this.$store.dispatch('type-map/configureType', { match: HCI.VMIMPORT_SOURCE_OVA, isCreatable: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return { rows: [] };
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
schema() {
|
|
||||||
return schema;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
typeDisplay() {
|
|
||||||
return this.$store.getters['type-map/labelFor'](schema, 99);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Loading v-if="$fetchState.pending" />
|
|
||||||
<ResourceTable
|
|
||||||
v-else
|
|
||||||
v-bind="$attrs"
|
|
||||||
:groupable="true"
|
|
||||||
:schema="schema"
|
|
||||||
:rows="rows"
|
|
||||||
key-field="_key"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<script>
|
|
||||||
import ResourceTable from '@shell/components/ResourceTable';
|
|
||||||
import Loading from '@shell/components/Loading';
|
|
||||||
import { SCHEMA } from '@shell/config/types';
|
|
||||||
import { HCI } from '../types';
|
|
||||||
|
|
||||||
const schema = {
|
|
||||||
id: HCI.VMIMPORT,
|
|
||||||
type: SCHEMA,
|
|
||||||
attributes: {
|
|
||||||
kind: HCI.VMIMPORT,
|
|
||||||
namespaced: true
|
|
||||||
},
|
|
||||||
metadata: { name: HCI.VMIMPORT },
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterVMImportVirtualMachine',
|
|
||||||
components: { ResourceTable, Loading },
|
|
||||||
inheritAttrs: false,
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIMPORT });
|
|
||||||
|
|
||||||
const configSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.VMIMPORT);
|
|
||||||
|
|
||||||
if (!configSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) {
|
|
||||||
this.$store.dispatch('type-map/configureType', { match: HCI.VMIMPORT, isCreatable: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return { rows: [] };
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
schema() {
|
|
||||||
return schema;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
typeDisplay() {
|
|
||||||
return this.$store.getters['type-map/labelFor'](schema, 99);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Loading v-if="$fetchState.pending" />
|
|
||||||
<ResourceTable
|
|
||||||
v-else
|
|
||||||
v-bind="$attrs"
|
|
||||||
:groupable="true"
|
|
||||||
:schema="schema"
|
|
||||||
:rows="rows"
|
|
||||||
key-field="_key"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user