mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-07-04 15:52:20 +00:00
Compare commits
69 Commits
main
...
v1.7.2-rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94be1c1c09 | ||
|
|
09469c67e2 | ||
|
|
b12d2b5df2 | ||
|
|
887a0d0517 | ||
|
|
73e91d19a4 | ||
|
|
bee30483ad | ||
|
|
71dad21796 | ||
|
|
5704240ac5 | ||
|
|
085d216a30 | ||
|
|
ff692affb0 | ||
|
|
cfb3392e2e | ||
|
|
fe42708361 | ||
|
|
62df6f5cbb | ||
|
|
2da7d0eb58 | ||
|
|
251d92afba | ||
|
|
7f3865343d | ||
|
|
8f7a153665 | ||
|
|
2a376933f2 | ||
|
|
e5c253f044 | ||
|
|
f0a9fe76a1 | ||
|
|
a0a797d5b9 | ||
|
|
1cc6f34d84 | ||
|
|
ebfc4e8369 | ||
|
|
bd647738ae | ||
|
|
c6fb969d7e | ||
|
|
2afb04947d | ||
|
|
730c68bf14 | ||
|
|
20bee39a6c | ||
|
|
dbb199d7bb | ||
|
|
5488979448 | ||
|
|
9e588e90c2 | ||
|
|
9378277102 | ||
|
|
b5e78018a5 | ||
|
|
f411a0c0af | ||
|
|
cfa58985cf | ||
|
|
66a8f9d0e7 | ||
|
|
bf61c7dd7d | ||
|
|
56d97260c4 | ||
|
|
beabb34920 | ||
|
|
d2609157bd | ||
|
|
8fbe1943d8 | ||
|
|
ec6bc4d639 | ||
|
|
3824a14730 | ||
|
|
0fc8bece02 | ||
|
|
39764af627 | ||
|
|
bdc87bda0e | ||
|
|
e0dc77624b | ||
|
|
c3ba10bd22 | ||
|
|
5b7d54d0a3 | ||
|
|
99a216dfa0 | ||
|
|
e4c85f510e | ||
|
|
f391f018de | ||
|
|
4b2e92ea15 | ||
|
|
2f956d5946 | ||
|
|
5f8d556ea2 | ||
|
|
396ab48f1c | ||
|
|
6b8c079018 | ||
|
|
7f638e86c8 | ||
|
|
0c4955a766 | ||
|
|
3f4ff30275 | ||
|
|
8e0332a364 | ||
|
|
6700b2055e | ||
|
|
e486852f7a | ||
|
|
c19341bec9 | ||
|
|
8cbb9d6b18 | ||
|
|
00f0953592 | ||
|
|
58507f0b2e | ||
|
|
7785d7f469 | ||
|
|
2c043e0a8e |
3
.github/actions/setup/action.yaml
vendored
3
.github/actions/setup/action.yaml
vendored
@ -4,8 +4,7 @@ description: Setup node and install dependencies
|
|||||||
runs:
|
runs:
|
||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Nodejs with yarn caching
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
|
|||||||
6
.github/auto-assign-config.yaml
vendored
Normal file
6
.github/auto-assign-config.yaml
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
addAssignees: author
|
||||||
|
addReviewers: true
|
||||||
|
numberOfReviewers: 0
|
||||||
|
reviewers:
|
||||||
|
- a110605
|
||||||
|
- houhoucoop
|
||||||
19
.github/renovate.json
vendored
19
.github/renovate.json
vendored
@ -7,24 +7,16 @@
|
|||||||
],
|
],
|
||||||
"baseBranches": [
|
"baseBranches": [
|
||||||
"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": "deps",
|
||||||
"prHourlyLimit": 12,
|
"prHourlyLimit": 12,
|
||||||
"digest": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"timezone": "Asia/Taipei",
|
"timezone": "Asia/Taipei",
|
||||||
"schedule": ["after 10am on sunday"],
|
"schedule": ["after 10am on sunday"],
|
||||||
"postUpdateOptions": ["yarnDedupeFewer"],
|
"postUpdateOptions": ["yarnDedupeFewer"],
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
|
||||||
"matchUpdateTypes": ["digest"],
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"matchUpdateTypes": ["major"],
|
"matchUpdateTypes": ["major"],
|
||||||
"enabled": false
|
"enabled": false
|
||||||
@ -46,12 +38,11 @@
|
|||||||
"reviewers": ["a110605", "houhoucoop"]
|
"reviewers": ["a110605", "houhoucoop"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchUpdateTypes": ["patch"],
|
"matchUpdateTypes": ["patch", "digest"],
|
||||||
"automerge": false,
|
"automerge": true,
|
||||||
"minimumReleaseAge": "7 days",
|
"minimumReleaseAge": "7 days",
|
||||||
"groupName": "patch dependencies",
|
"groupName": "patch digest dependencies",
|
||||||
"labels": ["patch-update"],
|
"labels": ["patch-update", "automerge"]
|
||||||
"reviewers": ["a110605", "houhoucoop"]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
39
.github/workflows/add-pr-label-check.yaml
vendored
39
.github/workflows/add-pr-label-check.yaml
vendored
@ -1,39 +0,0 @@
|
|||||||
name: "PR Management Add PR Label Collect Data"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, reopened, edited]
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- 'release-harvester-v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
collect:
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
actions: write
|
|
||||||
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: Extract PR label
|
|
||||||
run: |
|
|
||||||
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
|
|
||||||
echo "PR_LABEL=$PR_LABEL"
|
|
||||||
{
|
|
||||||
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
|
|
||||||
echo "PR_LABEL=$PR_LABEL"
|
|
||||||
} > pr-add-label-data.env
|
|
||||||
|
|
||||||
- name: Upload PR data artifact
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
||||||
with:
|
|
||||||
name: pr-add-label-data
|
|
||||||
path: pr-add-label-data.env
|
|
||||||
33
.github/workflows/add-pr-label.yaml
vendored
33
.github/workflows/add-pr-label.yaml
vendored
@ -1,33 +0,0 @@
|
|||||||
name: "PR Management Add PR Label"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows:
|
|
||||||
- "PR Management Add PR Label Collect Data"
|
|
||||||
types: [completed]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
auto-assign-pr-label:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- name: Download PR data artifact
|
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
||||||
with:
|
|
||||||
name: pr-add-label-data
|
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
|
||||||
github-token: ${{ github.token }}
|
|
||||||
|
|
||||||
- name: Load PR data
|
|
||||||
run: |
|
|
||||||
cat pr-add-label-data.env >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Set PR label
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
echo "PR_LABEL = $PR_LABEL"
|
|
||||||
gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label "$PR_LABEL"
|
|
||||||
28
.github/workflows/auto-assign-check.yaml
vendored
28
.github/workflows/auto-assign-check.yaml
vendored
@ -1,28 +0,0 @@
|
|||||||
name: "PR Management Auto Assign Collect Data"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, ready_for_review]
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- 'release-harvester-v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
collect:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
actions: write
|
|
||||||
steps:
|
|
||||||
- name: Save PR data to artifact
|
|
||||||
run: |
|
|
||||||
{
|
|
||||||
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
|
|
||||||
echo "PR_AUTHOR=${{ github.event.pull_request.user.login }}"
|
|
||||||
} > pr-auto-assign-data.env
|
|
||||||
|
|
||||||
- name: Upload PR data artifact
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
||||||
with:
|
|
||||||
name: pr-auto-assign-data
|
|
||||||
path: pr-auto-assign-data.env
|
|
||||||
17
.github/workflows/auto-assign-reviewer.yaml
vendored
Normal file
17
.github/workflows/auto-assign-reviewer.yaml
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
name: "[PR Management] Auto Assign Reviewer & Assignee"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, ready_for_review]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-assign:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: rancher/gh-issue-mgr/auto-assign-action@b70f0bdf12a03e5e3f33e4f92ccb6c89deb3ebd9 # main
|
||||||
|
with:
|
||||||
|
configuration-path: .github/auto-assign-config.yaml
|
||||||
35
.github/workflows/auto-assign.yaml
vendored
35
.github/workflows/auto-assign.yaml
vendored
@ -1,35 +0,0 @@
|
|||||||
name: "PR Management Auto Assign"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows:
|
|
||||||
- "PR Management Auto Assign Collect Data"
|
|
||||||
types: [completed]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
auto-assign:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
|
||||||
env:
|
|
||||||
REVIEWERS: "a110605,houhoucoop"
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- name: Download PR data artifact
|
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
||||||
with:
|
|
||||||
name: pr-auto-assign-data
|
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
|
||||||
github-token: ${{ github.token }}
|
|
||||||
|
|
||||||
- name: Load PR data
|
|
||||||
run: |
|
|
||||||
cat pr-auto-assign-data.env >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Auto assign PR author
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
echo "Assigning PR author: $PR_AUTHOR"
|
|
||||||
gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-assignee "$PR_AUTHOR" --add-reviewer "$REVIEWERS"
|
|
||||||
30
.github/workflows/backport-label-check.yaml
vendored
30
.github/workflows/backport-label-check.yaml
vendored
@ -1,30 +0,0 @@
|
|||||||
name: "PR Management Add Labels Collect Data"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, reopened]
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- 'release-harvester-v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
collect:
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
actions: write
|
|
||||||
steps:
|
|
||||||
- name: Save PR data to artifact
|
|
||||||
run: |
|
|
||||||
{
|
|
||||||
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
|
|
||||||
echo "PR_BASE_REF=${{ github.event.pull_request.base.ref }}"
|
|
||||||
echo "PR_USER_LOGIN=${{ github.event.pull_request.user.login }}"
|
|
||||||
} > pr-backport-label-data.env
|
|
||||||
|
|
||||||
- name: Upload PR data artifact
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
||||||
with:
|
|
||||||
name: pr-backport-label-data
|
|
||||||
path: pr-backport-label-data.env
|
|
||||||
99
.github/workflows/backport-label.yaml
vendored
99
.github/workflows/backport-label.yaml
vendored
@ -1,44 +1,40 @@
|
|||||||
name: "PR Management Add Backport Labels"
|
name: "[PR Management] Add Labels"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
pull_request_target:
|
||||||
workflows:
|
types: [opened, reopened]
|
||||||
- "PR Management Add Labels Collect Data"
|
branches:
|
||||||
types: [completed]
|
- main
|
||||||
|
- 'release-harvester-v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
add-require-backport-label:
|
add-require-backport-label:
|
||||||
|
if: github.event.pull_request.draft == false &&
|
||||||
|
github.event.pull_request.base.ref == 'main'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download PR data artifact
|
- name: Checkout repository
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
name: pr-backport-label-data
|
ref: ${{ github.base_ref }}
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
|
||||||
github-token: ${{ github.token }}
|
|
||||||
|
|
||||||
- name: Load PR data
|
- name: Fetch release branches and PR labels
|
||||||
run: |
|
id: fetch_info
|
||||||
cat pr-backport-label-data.env >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Add require-backport label (main branch PRs)
|
|
||||||
if: env.PR_BASE_REF == 'main'
|
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
repo="${{ github.repository }}"
|
repo="${{ github.repository }}"
|
||||||
pr_number="$PR_NUMBER"
|
pr_number=${{ github.event.pull_request.number }}
|
||||||
|
|
||||||
release_branches=$(gh api "repos/${repo}/branches" --paginate --jq '.[].name' | grep -E '^release-harvester-v[0-9]+\.[0-9]+$' || true)
|
release_branches=$(gh api "repos/${repo}/branches" --paginate --jq '.[].name' | grep -E '^release-harvester-v[0-9]+\.[0-9]+$' || true)
|
||||||
|
|
||||||
if [[ -z "$release_branches" ]]; then
|
if [[ -z "$release_branches" ]]; then
|
||||||
echo "No release branches found, skipping."
|
echo "should_label=false" >> "$GITHUB_OUTPUT"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -48,36 +44,51 @@ jobs:
|
|||||||
|
|
||||||
tags=$(gh api "repos/${repo}/releases" --paginate --jq '.[].tag_name')
|
tags=$(gh api "repos/${repo}/releases" --paginate --jq '.[].tag_name')
|
||||||
if echo "$tags" | grep -Fxq "$release_tag"; then
|
if echo "$tags" | grep -Fxq "$release_tag"; then
|
||||||
echo "Release $release_tag already published, skipping."
|
echo "should_label=false" >> "$GITHUB_OUTPUT"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
label="require backport/v${version}"
|
label="require backport/v${version}"
|
||||||
|
echo "should_label=true" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "backport_label=$label" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
pr_labels=$(gh pr view "$pr_number" --repo "$repo" --json labels --jq '.labels[].name' || echo "")
|
pr_labels=$(gh pr view "$pr_number" --repo "$repo" --json labels --jq '.labels[].name' || echo "")
|
||||||
|
pr_labels_csv=$(echo "$pr_labels" | tr '\n' ',' | sed 's/,$//')
|
||||||
|
echo "pr_labels=$pr_labels_csv" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
if echo "$pr_labels" | grep -Fxq "$label"; then
|
- name: Add label if needed
|
||||||
echo "Label '$label' already present, skipping."
|
if: steps.fetch_info.outputs.should_label == 'true' && !contains(steps.fetch_info.outputs.pr_labels, steps.fetch_info.outputs.backport_label)
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Adding label: $label"
|
|
||||||
gh pr edit "$pr_number" --repo "$repo" --add-label "$label"
|
|
||||||
|
|
||||||
- name: Add backport label (release branch PRs opened by Mergify)
|
|
||||||
if: startsWith(env.PR_BASE_REF, 'release-harvester-v')
|
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
echo "Adding label: ${{ steps.fetch_info.outputs.backport_label }}"
|
||||||
|
gh pr edit ${{ github.event.pull_request.number }} \
|
||||||
|
--repo ${{ github.repository }} \
|
||||||
|
--add-label "${{ steps.fetch_info.outputs.backport_label }}"
|
||||||
|
|
||||||
IS_MERGIFY=$(echo "$PR_USER_LOGIN" | grep -iq 'mergify' && echo true || echo false)
|
add-backport-label:
|
||||||
|
if: github.event.pull_request.draft == false &&
|
||||||
|
startsWith(github.event.pull_request.base.ref, 'release-harvester-v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check conditions for backport label
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
IS_MERGIFY=$(echo '${{ github.event.pull_request.user.login }}' | grep -iq 'mergify' && echo true || echo false)
|
||||||
|
TARGET_BRANCH=${{ github.event.pull_request.base.ref }}
|
||||||
|
|
||||||
if [[ "$IS_MERGIFY" != "true" ]]; then
|
echo "IS_MERGIFY=$IS_MERGIFY" >> $GITHUB_OUTPUT
|
||||||
echo "PR author is not Mergify, skipping."
|
echo "TARGET_BRANCH=$TARGET_BRANCH" >> $GITHUB_OUTPUT
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
version="${PR_BASE_REF#release-harvester-v}"
|
- name: Add label if needed
|
||||||
|
if: steps.check.outputs.IS_MERGIFY == 'true' && startsWith(steps.check.outputs.TARGET_BRANCH, 'release-harvester-v')
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
TARGET_BRANCH="${{ steps.check.outputs.TARGET_BRANCH }}"
|
||||||
|
version="${TARGET_BRANCH#release-harvester-v}"
|
||||||
label="backport/v${version}"
|
label="backport/v${version}"
|
||||||
echo "Adding label: $label"
|
echo "Adding label $label"
|
||||||
gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label "$label"
|
gh pr edit ${{ github.event.pull_request.number }} \
|
||||||
|
--repo ${{ github.repository }} \
|
||||||
|
--add-label "$label"
|
||||||
|
|||||||
28
.github/workflows/backport-request-check.yaml
vendored
28
.github/workflows/backport-request-check.yaml
vendored
@ -1,28 +0,0 @@
|
|||||||
name: "PR Management Request Backport via Mergify Collect Data"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
collect:
|
|
||||||
if: github.event.pull_request.merged == true
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
actions: write
|
|
||||||
steps:
|
|
||||||
- name: Save PR data to artifact
|
|
||||||
run: |
|
|
||||||
labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
|
|
||||||
{
|
|
||||||
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
|
|
||||||
echo "PR_LABELS=$(echo "$labels_json" | jq -r '[.[]] | join(",")')"
|
|
||||||
} > pr-backport-request-data.env
|
|
||||||
|
|
||||||
- name: Upload PR data artifact
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
||||||
with:
|
|
||||||
name: pr-backport-request-data
|
|
||||||
path: pr-backport-request-data.env
|
|
||||||
38
.github/workflows/backport-request.yaml
vendored
38
.github/workflows/backport-request.yaml
vendored
@ -1,47 +1,41 @@
|
|||||||
name: "PR Management Request Backport via Mergify"
|
name: "[PR Management] Request Backport via Mergify"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
pull_request_target:
|
||||||
workflows:
|
types: [closed]
|
||||||
- "PR Management Request Backport via Mergify Collect Data"
|
branches: [main]
|
||||||
types: [completed]
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
comment-backport:
|
comment-backport:
|
||||||
|
if: github.event.pull_request.merged == true
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download PR data artifact
|
- name: Checkout repository
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
name: pr-backport-request-data
|
ref: ${{ github.base_ref }}
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
|
||||||
github-token: ${{ github.token }}
|
|
||||||
|
|
||||||
- name: Load PR data
|
|
||||||
run: |
|
|
||||||
cat pr-backport-request-data.env >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Post Mergify backport command
|
- name: Post Mergify backport command
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
pr_number="$PR_NUMBER"
|
pr_number=${{ github.event.pull_request.number }}
|
||||||
repo="${{ github.repository }}"
|
repo="${{ github.repository }}"
|
||||||
|
|
||||||
labels=$(echo "$PR_LABELS" | tr ',' '\n')
|
labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
|
||||||
|
labels=$(echo "$labels_json" | jq -r '.[] // empty')
|
||||||
|
|
||||||
echo "Labels on PR: $labels"
|
echo "Labels on PR: $labels"
|
||||||
|
|
||||||
matches=$(echo "$labels" | grep -oE '^require backport/v[0-9]+\.[0-9]+$' || true)
|
matches=$(echo "$labels" | grep -oE '^require backport/v[0-9]+\.[0-9]+$' || true)
|
||||||
|
|
||||||
if [[ -z "$matches" ]]; then
|
if [[ -z "$matches" ]]; then
|
||||||
echo "No back-port labels found — skipping."
|
echo "No back‑port labels found — skipping."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
10
.github/workflows/build-extension-catalog.yml
vendored
10
.github/workflows/build-extension-catalog.yml
vendored
@ -55,8 +55,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: v3.8.0
|
version: v3.8.0
|
||||||
|
|
||||||
- name: Setup Nodejs with yarn install
|
- name: Setup Nodejs with yarn caching
|
||||||
uses: ./.github/actions/setup
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: yarn
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn
|
||||||
|
|
||||||
- name: Build and push UI image
|
- name: Build and push UI image
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@ -64,12 +64,6 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Setup Node from .nvmrc
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
cache: 'yarn'
|
|
||||||
|
|
||||||
- name: Setup environment
|
- name: Setup environment
|
||||||
run: |
|
run: |
|
||||||
corepack enable
|
corepack enable
|
||||||
|
|||||||
@ -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@9eb70a732e9be146722e1dbab431338366c2afc6 # creators-pkg-v3.0.10
|
||||||
permissions:
|
permissions:
|
||||||
actions: write
|
actions: write
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
30
.github/workflows/release-label.yaml
vendored
Normal file
30
.github/workflows/release-label.yaml
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
name: "[PR Management] Add PR Label"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, reopened, edited]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'release-harvester-v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-assign-pr-label:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.base_ref }}
|
||||||
|
- name: Setup Nodejs and yarn install
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
- name: Set PR label
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
|
||||||
|
echo "PR_LABEL = $PR_LABEL"
|
||||||
|
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "$PR_LABEL"
|
||||||
261
AGENTS.md
261
AGENTS.md
@ -1,261 +0,0 @@
|
|||||||
> This `./AGENTS.md` file is generated by running `yarn agents:generate`.
|
|
||||||
|
|
||||||
|
|
||||||
# Project Overview
|
|
||||||
|
|
||||||
The Harvester UI Extension is a Rancher extension that provides the user interface for Harvester within the 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.
|
|
||||||
```
|
|
||||||
|
|
||||||
# Personas
|
|
||||||
|
|
||||||
## Software Developer
|
|
||||||
|
|
||||||
You are an expert Senior Software Engineer specializing in Vue.js and TypeScript. You have deep knowledge of Kubernetes and the Rancher ecosystem.
|
|
||||||
- **Focus**: Writing clean, maintainable, and performant code.
|
|
||||||
- **Priorities**: Adhering to the project's code style, ensuring type safety, and following best practices for component design.
|
|
||||||
|
|
||||||
|
|
||||||
# Agents
|
|
||||||
|
|
||||||
## Boundaries
|
|
||||||
|
|
||||||
- ✅ **Always:**
|
|
||||||
- Write to `.github`, `docs/`, `pkg/harvester`, `scripts/`.
|
|
||||||
- Make commits in a new branch (for a PR).
|
|
||||||
- Run `yarn lint:fix` before commits.
|
|
||||||
- Use conventional commit format:
|
|
||||||
```
|
|
||||||
<type>:
|
|
||||||
|
|
||||||
<description>
|
|
||||||
```
|
|
||||||
- Follow existing naming conventions (PascalCase for components, camelCase for functions).
|
|
||||||
- After changing a Vue, JS, or TS file, make sure it's automatically formatted with ESLint.
|
|
||||||
- After updating files in `docs/agents.md/`, run `yarn agents:generate` to update root `AGENTS.md`.
|
|
||||||
- ⚠️ **Ask first:**
|
|
||||||
- Adding dependencies
|
|
||||||
- Upgrading dependencies
|
|
||||||
- 🚫 **Never:**
|
|
||||||
- Commit or log secrets, `.env`, `kubeconfig` or any API keys.
|
|
||||||
- Edit `node_modules/`.
|
|
||||||
- Commit directly to `main` (use PRs).
|
|
||||||
- Skip git hooks with `--no-verify` flag.
|
|
||||||
|
|
||||||
## Tools
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- **Node.js**: `>= 24.0.0` (see `.nvmrc` for the pinned version: `24`)
|
|
||||||
- **Package manager**: `yarn` (v1 classic)
|
|
||||||
|
|
||||||
### Common Commands
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
|---------|-------------|
|
|
||||||
| `yarn install --frozen-lockfile` | Install dependencies (CI-safe, no lockfile changes) |
|
|
||||||
| `RANCHER_ENV=harvester API=https://<harvester-ip> yarn dev` | Start development server at `https://127.0.0.1:8005` |
|
|
||||||
| `yarn build-pkg harvester` | Build the Harvester extension package |
|
|
||||||
| `yarn serve-pkgs` | Serve the locally built extension for testing |
|
|
||||||
| `yarn lint` | Run ESLint (zero warnings allowed) |
|
|
||||||
| `yarn lint:fix` | Run ESLint with auto-fix |
|
|
||||||
| `yarn clean` | Clean build artifacts |
|
|
||||||
| `yarn agents:generate` | Regenerate `AGENTS.md` from `docs/agents.md/` sources |
|
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
- **Start dev server**:
|
|
||||||
```bash
|
|
||||||
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
|
||||||
```
|
|
||||||
- `API` should point to a running Harvester cluster (e.g., `https://x.x.x.x`).
|
|
||||||
- The dashboard will be available at `https://127.0.0.1:8005`.
|
|
||||||
|
|
||||||
### Building
|
|
||||||
|
|
||||||
- **Build extension package**:
|
|
||||||
```bash
|
|
||||||
yarn build-pkg harvester
|
|
||||||
```
|
|
||||||
- **Serve locally built extension** (for integration testing with a Rancher instance):
|
|
||||||
```bash
|
|
||||||
yarn serve-pkgs
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linting
|
|
||||||
|
|
||||||
- **Check** (must pass with zero warnings):
|
|
||||||
```bash
|
|
||||||
yarn lint
|
|
||||||
```
|
|
||||||
- **Auto-fix**:
|
|
||||||
```bash
|
|
||||||
yarn lint:fix
|
|
||||||
```
|
|
||||||
- ESLint covers `.js`, `.ts`, and `.vue` files.
|
|
||||||
- Always run `yarn lint:fix` before committing.
|
|
||||||
|
|
||||||
### Commit Conventions
|
|
||||||
|
|
||||||
- Commits are validated by [commitlint](https://commitlint.js.org/) via a Husky `commit-msg` hook.
|
|
||||||
- Follow [Conventional Commits](https://www.conventionalcommits.org/) format (configured in `commitlint.config.js`).
|
|
||||||
|
|
||||||
### Agent Documentation
|
|
||||||
|
|
||||||
- Source files live in `docs/agents.md/` (agents, contributors, personas subdirectories).
|
|
||||||
- After editing any source file, regenerate the root `AGENTS.md`:
|
|
||||||
```bash
|
|
||||||
yarn agents:generate
|
|
||||||
```
|
|
||||||
|
|
||||||
# Contributors Guide
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
Please see the [Harvester UI Extension README](https://github.com/harvester/harvester-ui-extension).
|
|
||||||
|
|
||||||
To get started, follow the `Development Setup` section.
|
|
||||||
|
|
||||||
## Project Information
|
|
||||||
|
|
||||||
- **Tech Stack:**
|
|
||||||
- `Vue.js`: Framework
|
|
||||||
- `Linting`: ESLint
|
|
||||||
- `CSS`: SCSS should be used
|
|
||||||
- `TypeScript`: Primary language for logic.
|
|
||||||
- **Code Style and Standards:**
|
|
||||||
- `Language`: TypeScript is preferred for new code.
|
|
||||||
- `Vue.js`:
|
|
||||||
- Composition API components are preferred over Options API.
|
|
||||||
- Large pages with lots of code and styles should be avoided by breaking the page up into smaller Vue components.
|
|
||||||
- Place source tag above template above style.
|
|
||||||
- style tag should contain `lang='scss' scoped`.
|
|
||||||
- `Linting`: Follow the ESLint configuration in the root.
|
|
||||||
|
|
||||||
- **File Structure:**
|
|
||||||
- `.github/`: CI/CD workflows and Renovate config.
|
|
||||||
- `docs/`: Documentation source for `AGENTS.md` generation.
|
|
||||||
- `scripts/`: Bash scripts for build, CI and doc generation.
|
|
||||||
- `pkg/harvester/`: Main extension source. Files are named after K8s resource types (e.g., `kubevirt.io.virtualmachine`, `harvesterhci.io.volume`).
|
|
||||||
- `index.ts`: Plugin entry point — registers the product and auto-imports models/detail/edit views.
|
|
||||||
- `types.ts`: `HCI` constant mapping 60+ K8s resource types to string identifiers.
|
|
||||||
- `components/`: Reusable Vue components (VNC/serial console, settings panels, upgrade banners, filters).
|
|
||||||
- `config/`: Constants — settings keys, table column definitions, feature flags, type mappings, doc links.
|
|
||||||
- `detail/`: Read-only detail views per resource type. Complex resources use subdirectories with tabs.
|
|
||||||
- `dialog/`: Modal dialogs for operations (VM clone/migrate/restart, backup/restore, device passthrough, etc.).
|
|
||||||
- `edit/`: Create/edit forms. Complex resources (e.g., VM) split into subcomponents (CpuMemory, Network, Volume, CloudConfig, etc.).
|
|
||||||
- `formatters/`: Table cell formatters — state badges, usage bars, resource references.
|
|
||||||
- `l10n/`: Localization (`en-us.yaml`).
|
|
||||||
- `list/`: List (table) views per resource type, mirroring `detail/` and `edit/` naming.
|
|
||||||
- `mixins/`: Shared Vue mixins — VM helpers (`harvester-vm/`) and disk helpers (`harvester-disk.js`).
|
|
||||||
- `models/`: Model classes extending `SteveModel` with computed properties and actions. Base class: `harvester.js`.
|
|
||||||
- `pages/`: Route-level pages — dashboard, support, console, members, brand, alertmanager.
|
|
||||||
- `promptRemove/`: Custom delete-confirmation dialogs (VM, backup).
|
|
||||||
- `routing/`: Vue Router config — all product routes (CRUD, console, support, upgrade, etc.).
|
|
||||||
- `store/`: Vuex modules — `harvester-common.js` for shared state; `harvester-store/` for VM/resource creation actions with Steve integration.
|
|
||||||
- `styles/`: Global SCSS files.
|
|
||||||
- `utils/`: Helpers — VM volume templates, CPU/memory calc, cron parsing, regex validators, feature flags.
|
|
||||||
- `validators/`: Form validation functions per resource type, pushing i18n error messages.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Harvester UI Extension Development Guide
|
|
||||||
|
|
||||||
1. Backward Compatibility
|
|
||||||
The Harvester UI Extension supports earlier cluster versions (e.g., UI Ext v1.8.0 works with clusters v1.7.0 and v1.6.0). It uses Feature Flags defined in pkg/config/feature-flags to ensure the UI matches the cluster's specific version.
|
|
||||||
|
|
||||||
2. Implementation Steps for New Features
|
|
||||||
To add a feature in a new release, follow these steps:
|
|
||||||
|
|
||||||
Register: Add a unique [Feature Name] to the corresponding release array in the configuration.
|
|
||||||
|
|
||||||
Check: Use the following getter to verify if the feature is enabled for the current version:
|
|
||||||
|
|
||||||
```
|
|
||||||
computed: {
|
|
||||||
newFeatureEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('[Feature Name]');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
```
|
|
||||||
Render: Use the result of the check to conditionally render the UI components.
|
|
||||||
|
|
||||||
|
|
||||||
## E2E Tests (Cypress)
|
|
||||||
|
|
||||||
See https://github.com/harvester/harvester-ui-tests
|
|
||||||
|
|
||||||
## Node Dependencies
|
|
||||||
|
|
||||||
Dependencies are managed via `package.json` and `yarn`
|
|
||||||
|
|
||||||
- To install dependencies use `yarn install`. This will fail if dependencies and versions listed in `package.json` are out of step with the `yarn.lock` file
|
|
||||||
- To add a dependency use `yarn run add:no-lock ...` instead of `yarn add`
|
|
||||||
- To upgrade a dependency use `yarn run upgrade:no-lock ...` instead of `yarn upgrade`
|
|
||||||
|
|
||||||
|
|
||||||
Renovate automatically updates dependencies and opens upgrade PRs after 10:00 AM on Sundays (`Asia/Taipei`).
|
|
||||||
For the Renovate config, see `.github/renovate.json`.
|
|
||||||
|
|
||||||
|
|
||||||
## Milestone guidance
|
|
||||||
|
|
||||||
- All issues must first be resolved in the `main` branch
|
|
||||||
- If backports are needed they can be made via the backport bot
|
|
||||||
- pull requests
|
|
||||||
- comment `@Mergifyio backport <target branch>` e.g. `@Mergifyio backport release-harvester-v1.8`
|
|
||||||
- All backported pull requests must link to a backported issue
|
|
||||||
|
|
||||||
|
|
||||||
## Creating a branch
|
|
||||||
|
|
||||||
### To resolve an issue
|
|
||||||
- Checkout the branch matching the milestone of the issue `git checkout ${targetMilestoneBranch}`. Replace `${targetMilestoneBranch}` with the target milestone of the issue. For example
|
|
||||||
- `main` for the latest unreleased minor version
|
|
||||||
- `release-harvester-v.X` for release minor versions
|
|
||||||
- `release-harvester-v1.6`
|
|
||||||
- `release-harvester-v1.7`
|
|
||||||
- `release-harvester-v1.8`
|
|
||||||
- Ensure you have the latest of that branch `git pull --rebase`
|
|
||||||
- Checkout the branch to commit the changes to `git checkout issue-${issueNumber}`. Replace `${issueNumber}` with the issue number.
|
|
||||||
|
|
||||||
## Creating a commit
|
|
||||||
|
|
||||||
- This project uses commit-lint with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) 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
|
|
||||||
|
|
||||||
## Creating a Pull Request
|
|
||||||
|
|
||||||
- Pull requests must come from forks
|
|
||||||
- Description should always have commit supported type prefix. E.g `fix: XXX`, `feat: OOO`
|
|
||||||
- A Pull Request will only be merged once
|
|
||||||
- ALL CI gates have passed
|
|
||||||
- At least one harvester/harvester-ui-extension code owners reviews and approves the PR
|
|
||||||
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
@a110605
|
|
||||||
@houhoucoop
|
|
||||||
24
README.md
24
README.md
@ -5,22 +5,9 @@ The Harvester UI Extension is a Rancher extension that provides the user interfa
|
|||||||
> **Note:**
|
> **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.
|
> This extension is available starting from **Rancher 2.10.0**. Ensure your Rancher version is **2.10.0 or later** to access Harvester integration.
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [Installation](#installation)
|
|
||||||
- [Development Setup](#development-setup)
|
|
||||||
- [Commit Message Guidelines](#commit-message-guidelines)
|
|
||||||
- [Branch Structure](#branch-structure)
|
|
||||||
- [Testing Guidelines](#testing-guidelines)
|
|
||||||
- [Release](#release)
|
|
||||||
- [Contributing](#contributing)
|
|
||||||
- [License](#license)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 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
|
||||||
@ -170,20 +157,13 @@ To test the standalone UI, configure Harvester to load the UI from an external s
|
|||||||
2. Set **ui-source** to `External`
|
2. Set **ui-source** to `External`
|
||||||
3. Set **ui-index** to the desired URL
|
3. Set **ui-index** to the desired URL
|
||||||
|
|
||||||
|
|
||||||
## Release
|
|
||||||
|
|
||||||
The Harvester UI Extension follows the [Harvester](https://github.com/harvester/harvester) release cycle. After RC1 is cut for a new Harvester version, we usually create and work from the corresponding release branch (for example, `release-harvester-v1.8`). The remaining RC builds and the final official release are published from that branch.
|
|
||||||
|
|
||||||
After Harvester releases a new version, update the Harvester entry in rancher/ui-plugin-charts [manifest.json](https://github.com/rancher/ui-plugin-charts/blob/aafd215debbc6cb3100e7ba4b0a542c932397acd/manifest.json#L133-L151). This ensures air-gapped users can pull the new Harvester UI Extension image.
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
If you want to contribute, start by reading this document, then visit our [Getting Started guide](https://extensions.rancher.io/extensions/next/extensions-getting-started) to learn how to develop and submit changes.
|
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
|
## 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.
|
||||||
|
|||||||
@ -33,7 +33,7 @@ module.exports = {
|
|||||||
'subject-full-stop': [2, 'never', '.'],
|
'subject-full-stop': [2, 'never', '.'],
|
||||||
'subject-max-length': [0, 'never'],
|
'subject-max-length': [0, 'never'],
|
||||||
'body-leading-blank': [2, 'always'],
|
'body-leading-blank': [2, 'always'],
|
||||||
'body-max-line-length': [0, 'always', 100],
|
'body-max-line-length': [2, 'always', 100],
|
||||||
'footer-leading-blank': [2, 'always'],
|
'footer-leading-blank': [2, 'always'],
|
||||||
'footer-max-line-length': [2, 'always', 100],
|
'footer-max-line-length': [2, 'always', 100],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
# Agents.md
|
|
||||||
|
|
||||||
This folder contains files used to construct a single root `AGENTS.md`
|
|
||||||
|
|
||||||
We do this because
|
|
||||||
- The single commonality between almost all AI tools is that they look for a root `AGENTS.md`
|
|
||||||
- Having the information in a single place for all AI tools is better than duplicating and maintaining the information in multiple files for each AI tool
|
|
||||||
|
|
||||||
## Process
|
|
||||||
1. Add files to or update files in `./docs/agents.md/agents`, `./docs/agents.md/personas`, `./docs/agents.md/contributors`.
|
|
||||||
2. Each file must
|
|
||||||
- Start markdown headers at the `##` level
|
|
||||||
- End in a new line
|
|
||||||
- Order contents in AGENTS.md by giving an optional number prefix
|
|
||||||
3. Run `yarn agents:generate`.
|
|
||||||
- Take the contents of `./docs/agents.md/template_agents.md`
|
|
||||||
- Insert the contents of the agents, personas and contributors files
|
|
||||||
- Output to `./AGENTS.md`
|
|
||||||
4. Commit all changes, including root `./AGENTS.md` and any files that link to it
|
|
||||||
|
|
||||||
## Guidance
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
When referencing code or configuration it's good to include an example
|
|
||||||
|
|
||||||
## Future Tasks
|
|
||||||
- Add real world examples when referencing code
|
|
||||||
- Create a GH workflow to monitor `agents.md` folder files in a PR, if changed run generate script and add new AGENTS.md to PR
|
|
||||||
- Discuss
|
|
||||||
- Three multi AI tool related questions
|
|
||||||
- Questions
|
|
||||||
- how AI specific patterns, like co-pilot instructions, can be expressed in a way to be adopted by or reduce duplication with the root agents.md
|
|
||||||
- how AI tool specific metadata code can be included
|
|
||||||
- multiple agents vs personas approach works
|
|
||||||
- Proposal
|
|
||||||
- Generic information applicable to all AI tools is added to the root `agents.md`
|
|
||||||
- Agent, instruction or other AI specific files or patterns can be used in files specific to that AI tool. They must not contain duplicate information to the root `agents.md`
|
|
||||||
- The root `agents.md` continues to be AI tool agnostic
|
|
||||||
- How can folder specific agents be used
|
|
||||||
- Proposal
|
|
||||||
- `agents.md` are added to folders as long as most LLMs can make use of them and they do not duplicate information in the root `agents.md`
|
|
||||||
|
|
||||||
## Test Prompts
|
|
||||||
|
|
||||||
To validate LLMs are picking up information consider running the following prompts.
|
|
||||||
|
|
||||||
### Information
|
|
||||||
|
|
||||||
Can they provide correct information?
|
|
||||||
|
|
||||||
- `what's this project about?`
|
|
||||||
- `can you tell me the process for fixing a github issue in this repo?`
|
|
||||||
- `what makes a good software developer?`
|
|
||||||
- `what should never be done in this project?`
|
|
||||||
- `what are the folders in this project for?`
|
|
||||||
- `what are the env vars i could use when running e2e tests?`
|
|
||||||
|
|
||||||
### Actions
|
|
||||||
|
|
||||||
Can they use the information in their actions?
|
|
||||||
|
|
||||||
Pending
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
## Boundaries
|
|
||||||
|
|
||||||
- ✅ **Always:**
|
|
||||||
- Write to `.github`, `docs/`, `pkg/harvester`, `scripts/`.
|
|
||||||
- Make commits in a new branch (for a PR).
|
|
||||||
- Run `yarn lint:fix` before commits.
|
|
||||||
- Use conventional commit format:
|
|
||||||
```
|
|
||||||
<type>:
|
|
||||||
|
|
||||||
<description>
|
|
||||||
```
|
|
||||||
- Follow existing naming conventions (PascalCase for components, camelCase for functions).
|
|
||||||
- After changing a Vue, JS, or TS file, make sure it's automatically formatted with ESLint.
|
|
||||||
- After updating files in `docs/agents.md/`, run `yarn agents:generate` to update root `AGENTS.md`.
|
|
||||||
- ⚠️ **Ask first:**
|
|
||||||
- Adding dependencies
|
|
||||||
- Upgrading dependencies
|
|
||||||
- 🚫 **Never:**
|
|
||||||
- Commit or log secrets, `.env`, `kubeconfig` or any API keys.
|
|
||||||
- Edit `node_modules/`.
|
|
||||||
- Commit directly to `main` (use PRs).
|
|
||||||
- Skip git hooks with `--no-verify` flag.
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
## Tools
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- **Node.js**: `>= 24.0.0` (see `.nvmrc` for the pinned version: `24`)
|
|
||||||
- **Package manager**: `yarn` (v1 classic)
|
|
||||||
|
|
||||||
### Common Commands
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
|---------|-------------|
|
|
||||||
| `yarn install --frozen-lockfile` | Install dependencies (CI-safe, no lockfile changes) |
|
|
||||||
| `RANCHER_ENV=harvester API=https://<harvester-ip> yarn dev` | Start development server at `https://127.0.0.1:8005` |
|
|
||||||
| `yarn build-pkg harvester` | Build the Harvester extension package |
|
|
||||||
| `yarn serve-pkgs` | Serve the locally built extension for testing |
|
|
||||||
| `yarn lint` | Run ESLint (zero warnings allowed) |
|
|
||||||
| `yarn lint:fix` | Run ESLint with auto-fix |
|
|
||||||
| `yarn clean` | Clean build artifacts |
|
|
||||||
| `yarn agents:generate` | Regenerate `AGENTS.md` from `docs/agents.md/` sources |
|
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
- **Start dev server**:
|
|
||||||
```bash
|
|
||||||
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
|
||||||
```
|
|
||||||
- `API` should point to a running Harvester cluster (e.g., `https://x.x.x.x`).
|
|
||||||
- The dashboard will be available at `https://127.0.0.1:8005`.
|
|
||||||
|
|
||||||
### Building
|
|
||||||
|
|
||||||
- **Build extension package**:
|
|
||||||
```bash
|
|
||||||
yarn build-pkg harvester
|
|
||||||
```
|
|
||||||
- **Serve locally built extension** (for integration testing with a Rancher instance):
|
|
||||||
```bash
|
|
||||||
yarn serve-pkgs
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linting
|
|
||||||
|
|
||||||
- **Check** (must pass with zero warnings):
|
|
||||||
```bash
|
|
||||||
yarn lint
|
|
||||||
```
|
|
||||||
- **Auto-fix**:
|
|
||||||
```bash
|
|
||||||
yarn lint:fix
|
|
||||||
```
|
|
||||||
- ESLint covers `.js`, `.ts`, and `.vue` files.
|
|
||||||
- Always run `yarn lint:fix` before committing.
|
|
||||||
|
|
||||||
### Commit Conventions
|
|
||||||
|
|
||||||
- Commits are validated by [commitlint](https://commitlint.js.org/) via a Husky `commit-msg` hook.
|
|
||||||
- Follow [Conventional Commits](https://www.conventionalcommits.org/) format (configured in `commitlint.config.js`).
|
|
||||||
|
|
||||||
### Agent Documentation
|
|
||||||
|
|
||||||
- Source files live in `docs/agents.md/` (agents, contributors, personas subdirectories).
|
|
||||||
- After editing any source file, regenerate the root `AGENTS.md`:
|
|
||||||
```bash
|
|
||||||
yarn agents:generate
|
|
||||||
```
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
## Getting Started
|
|
||||||
|
|
||||||
Please see the [Harvester UI Extension README](https://github.com/harvester/harvester-ui-extension).
|
|
||||||
|
|
||||||
To get started, follow the `Development Setup` section.
|
|
||||||
|
|
||||||
## Project Information
|
|
||||||
|
|
||||||
- **Tech Stack:**
|
|
||||||
- `Vue.js`: Framework
|
|
||||||
- `Linting`: ESLint
|
|
||||||
- `CSS`: SCSS should be used
|
|
||||||
- `TypeScript`: Primary language for logic.
|
|
||||||
- **Code Style and Standards:**
|
|
||||||
- `Language`: TypeScript is preferred for new code.
|
|
||||||
- `Vue.js`:
|
|
||||||
- Composition API components are preferred over Options API.
|
|
||||||
- Large pages with lots of code and styles should be avoided by breaking the page up into smaller Vue components.
|
|
||||||
- Place source tag above template above style.
|
|
||||||
- style tag should contain `lang='scss' scoped`.
|
|
||||||
- `Linting`: Follow the ESLint configuration in the root.
|
|
||||||
|
|
||||||
- **File Structure:**
|
|
||||||
- `.github/`: CI/CD workflows and Renovate config.
|
|
||||||
- `docs/`: Documentation source for `AGENTS.md` generation.
|
|
||||||
- `scripts/`: Bash scripts for build, CI and doc generation.
|
|
||||||
- `pkg/harvester/`: Main extension source. Files are named after K8s resource types (e.g., `kubevirt.io.virtualmachine`, `harvesterhci.io.volume`).
|
|
||||||
- `index.ts`: Plugin entry point — registers the product and auto-imports models/detail/edit views.
|
|
||||||
- `types.ts`: `HCI` constant mapping 60+ K8s resource types to string identifiers.
|
|
||||||
- `components/`: Reusable Vue components (VNC/serial console, settings panels, upgrade banners, filters).
|
|
||||||
- `config/`: Constants — settings keys, table column definitions, feature flags, type mappings, doc links.
|
|
||||||
- `detail/`: Read-only detail views per resource type. Complex resources use subdirectories with tabs.
|
|
||||||
- `dialog/`: Modal dialogs for operations (VM clone/migrate/restart, backup/restore, device passthrough, etc.).
|
|
||||||
- `edit/`: Create/edit forms. Complex resources (e.g., VM) split into subcomponents (CpuMemory, Network, Volume, CloudConfig, etc.).
|
|
||||||
- `formatters/`: Table cell formatters — state badges, usage bars, resource references.
|
|
||||||
- `l10n/`: Localization (`en-us.yaml`).
|
|
||||||
- `list/`: List (table) views per resource type, mirroring `detail/` and `edit/` naming.
|
|
||||||
- `mixins/`: Shared Vue mixins — VM helpers (`harvester-vm/`) and disk helpers (`harvester-disk.js`).
|
|
||||||
- `models/`: Model classes extending `SteveModel` with computed properties and actions. Base class: `harvester.js`.
|
|
||||||
- `pages/`: Route-level pages — dashboard, support, console, members, brand, alertmanager.
|
|
||||||
- `promptRemove/`: Custom delete-confirmation dialogs (VM, backup).
|
|
||||||
- `routing/`: Vue Router config — all product routes (CRUD, console, support, upgrade, etc.).
|
|
||||||
- `store/`: Vuex modules — `harvester-common.js` for shared state; `harvester-store/` for VM/resource creation actions with Steve integration.
|
|
||||||
- `styles/`: Global SCSS files.
|
|
||||||
- `utils/`: Helpers — VM volume templates, CPU/memory calc, cron parsing, regex validators, feature flags.
|
|
||||||
- `validators/`: Form validation functions per resource type, pushing i18n error messages.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Harvester UI Extension Development Guide
|
|
||||||
|
|
||||||
1. Backward Compatibility
|
|
||||||
The Harvester UI Extension supports earlier cluster versions (e.g., UI Ext v1.8.0 works with clusters v1.7.0 and v1.6.0). It uses Feature Flags defined in pkg/config/feature-flags to ensure the UI matches the cluster's specific version.
|
|
||||||
|
|
||||||
2. Implementation Steps for New Features
|
|
||||||
To add a feature in a new release, follow these steps:
|
|
||||||
|
|
||||||
Register: Add a unique [Feature Name] to the corresponding release array in the configuration.
|
|
||||||
|
|
||||||
Check: Use the following getter to verify if the feature is enabled for the current version:
|
|
||||||
|
|
||||||
```
|
|
||||||
computed: {
|
|
||||||
newFeatureEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('[Feature Name]');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
```
|
|
||||||
Render: Use the result of the check to conditionally render the UI components.
|
|
||||||
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
## E2E Tests (Cypress)
|
|
||||||
|
|
||||||
See https://github.com/harvester/harvester-ui-tests
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
## Node Dependencies
|
|
||||||
|
|
||||||
Dependencies are managed via `package.json` and `yarn`
|
|
||||||
|
|
||||||
- To install dependencies use `yarn install`. This will fail if dependencies and versions listed in `package.json` are out of step with the `yarn.lock` file
|
|
||||||
- To add a dependency use `yarn run add:no-lock ...` instead of `yarn add`
|
|
||||||
- To upgrade a dependency use `yarn run upgrade:no-lock ...` instead of `yarn upgrade`
|
|
||||||
|
|
||||||
|
|
||||||
Renovate automatically updates dependencies and opens upgrade PRs after 10:00 AM on Sundays (`Asia/Taipei`).
|
|
||||||
For the Renovate config, see `.github/renovate.json`.
|
|
||||||
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
## Milestone guidance
|
|
||||||
|
|
||||||
- All issues must first be resolved in the `main` branch
|
|
||||||
- If backports are needed they can be made via the backport bot
|
|
||||||
- pull requests
|
|
||||||
- comment `@Mergifyio backport <target branch>` e.g. `@Mergifyio backport release-harvester-v1.8`
|
|
||||||
- All backported pull requests must link to a backported issue
|
|
||||||
|
|
||||||
|
|
||||||
## Creating a branch
|
|
||||||
|
|
||||||
### To resolve an issue
|
|
||||||
- Checkout the branch matching the milestone of the issue `git checkout ${targetMilestoneBranch}`. Replace `${targetMilestoneBranch}` with the target milestone of the issue. For example
|
|
||||||
- `main` for the latest unreleased minor version
|
|
||||||
- `release-harvester-v.X` for release minor versions
|
|
||||||
- `release-harvester-v1.6`
|
|
||||||
- `release-harvester-v1.7`
|
|
||||||
- `release-harvester-v1.8`
|
|
||||||
- Ensure you have the latest of that branch `git pull --rebase`
|
|
||||||
- Checkout the branch to commit the changes to `git checkout issue-${issueNumber}`. Replace `${issueNumber}` with the issue number.
|
|
||||||
|
|
||||||
## Creating a commit
|
|
||||||
|
|
||||||
- This project uses commit-lint with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) 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
|
|
||||||
|
|
||||||
## Creating a Pull Request
|
|
||||||
|
|
||||||
- Pull requests must come from forks
|
|
||||||
- Description should always have commit supported type prefix. E.g `fix: XXX`, `feat: OOO`
|
|
||||||
- A Pull Request will only be merged once
|
|
||||||
- ALL CI gates have passed
|
|
||||||
- At least one harvester/harvester-ui-extension code owners reviews and approves the PR
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
## Software Developer
|
|
||||||
|
|
||||||
You are an expert Senior Software Engineer specializing in Vue.js and TypeScript. You have deep knowledge of Kubernetes and the Rancher ecosystem.
|
|
||||||
- **Focus**: Writing clean, maintainable, and performant code.
|
|
||||||
- **Priorities**: Adhering to the project's code style, ensuring type safety, and following best practices for component design.
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
> This `./AGENTS.md` file is generated by running `yarn agents:generate`.
|
|
||||||
|
|
||||||
|
|
||||||
# Project Overview
|
|
||||||
|
|
||||||
The Harvester UI Extension is a Rancher extension that provides the user interface for Harvester within the 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.
|
|
||||||
```
|
|
||||||
|
|
||||||
# Personas
|
|
||||||
|
|
||||||
`<personas>`
|
|
||||||
|
|
||||||
# Agents
|
|
||||||
|
|
||||||
`<agents>`
|
|
||||||
|
|
||||||
# Contributors Guide
|
|
||||||
|
|
||||||
`<contributors>`
|
|
||||||
21
package.json
21
package.json
@ -1,27 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.9.0-dev",
|
"version": "1.7.2-rc2",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-class-static-block": "7.29.7",
|
"@babel/plugin-transform-class-static-block": "7.29.7",
|
||||||
"@rancher/shell": "3.0.12-rc.3",
|
"@rancher/shell": "3.0.8-rc.8",
|
||||||
"@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": "25.9.4",
|
"@types/node": "~20.19.0",
|
||||||
"cronstrue": "2.59.0",
|
"cronstrue": "2.59.0",
|
||||||
"d3-color": "3.1.0",
|
"d3-color": "3.1.0",
|
||||||
"ejs": "3.1.10",
|
"ejs": "3.1.10",
|
||||||
@ -34,8 +29,8 @@
|
|||||||
"node-forge": "1.4.0",
|
"node-forge": "1.4.0",
|
||||||
"nth-check": "2.1.1",
|
"nth-check": "2.1.1",
|
||||||
"qs": "6.15.2",
|
"qs": "6.15.2",
|
||||||
"roarr": "7.21.6",
|
"roarr": "7.21.5",
|
||||||
"semver": "7.8.5",
|
"semver": "7.8.2",
|
||||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -43,14 +38,12 @@
|
|||||||
"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",
|
||||||
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
|
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
|
||||||
"commitlint": "commitlint --edit",
|
"commitlint": "commitlint --edit",
|
||||||
"prepare": "husky",
|
"prepare": "husky"
|
||||||
"agents:generate": "./scripts/generate-agent-and-persona-mds.sh"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/load": "^19.8.1",
|
"@commitlint/load": "^19.8.1",
|
||||||
|
|||||||
@ -7,7 +7,8 @@ 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
|
||||||
|
|
||||||
@ -162,7 +163,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.
|
||||||
|
|||||||
@ -1,186 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import MatchExpressions from '@shell/components/form/MatchExpressions';
|
|
||||||
import ResourceTable from '@shell/components/ResourceTable';
|
|
||||||
import { _EDIT } from '@shell/config/query-params';
|
|
||||||
import { convert, simplify, matching as selectorMatching } from '@shell/utils/selector';
|
|
||||||
import throttle from 'lodash/throttle';
|
|
||||||
import { NODE } from '@shell/config/types';
|
|
||||||
import { NAME, AGE } from '@shell/config/table-headers';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterNodeSelector',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
Banner,
|
|
||||||
MatchExpressions,
|
|
||||||
ResourceTable,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
default: _EDIT,
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
this.updateMatchingResources();
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
return {
|
|
||||||
matchingResources: {
|
|
||||||
matched: 0,
|
|
||||||
matches: [],
|
|
||||||
none: true,
|
|
||||||
sample: null,
|
|
||||||
total: 0,
|
|
||||||
},
|
|
||||||
tableHeaders: [
|
|
||||||
NAME,
|
|
||||||
{
|
|
||||||
name: 'host-ip',
|
|
||||||
labelKey: 'tableHeaders.hostIp',
|
|
||||||
search: ['internalIp'],
|
|
||||||
value: 'internalIp',
|
|
||||||
sort: ['internalIp'],
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'cpuManager',
|
|
||||||
labelKey: 'harvester.tableHeaders.cpuManager',
|
|
||||||
value: 'id',
|
|
||||||
formatter: 'HarvesterCPUPinning',
|
|
||||||
width: 150,
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'diskState',
|
|
||||||
labelKey: 'tableHeaders.diskState',
|
|
||||||
value: 'diskState',
|
|
||||||
formatter: 'HarvesterDiskState',
|
|
||||||
width: 130,
|
|
||||||
},
|
|
||||||
AGE,
|
|
||||||
],
|
|
||||||
inStore,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
value: {
|
|
||||||
handler: 'updateMatchingResources',
|
|
||||||
deep: true,
|
|
||||||
},
|
|
||||||
allResourcesInScope() {
|
|
||||||
this.updateMatchingResources();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
schema() {
|
|
||||||
return this.$store.getters[`${ this.inStore }/schemaFor`](NODE);
|
|
||||||
},
|
|
||||||
|
|
||||||
selectorExpressions: {
|
|
||||||
get() {
|
|
||||||
return convert(
|
|
||||||
this.value.matchLabels || {},
|
|
||||||
this.value.matchExpressions || []
|
|
||||||
);
|
|
||||||
},
|
|
||||||
set(selectorExpressions) {
|
|
||||||
const { matchLabels, matchExpressions } = simplify(selectorExpressions);
|
|
||||||
|
|
||||||
this.value['matchLabels'] = matchLabels;
|
|
||||||
this.value['matchExpressions'] = matchExpressions;
|
|
||||||
this.updateMatchingResources();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
allNodes() {
|
|
||||||
return this.$store.getters[`${ this.inStore }/all`](NODE) || [];
|
|
||||||
},
|
|
||||||
|
|
||||||
allResourcesInScope() {
|
|
||||||
return this.allNodes.length;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
updateMatchingResources: throttle(function() {
|
|
||||||
const expressions = this.selectorExpressions;
|
|
||||||
const allNodes = this.allNodes;
|
|
||||||
|
|
||||||
// Empty expressions with no key = no match
|
|
||||||
const hasValidExpression = expressions.length > 0 && expressions.every((e) => !!e.key);
|
|
||||||
|
|
||||||
if (!hasValidExpression) {
|
|
||||||
this.matchingResources = {
|
|
||||||
matched: 0,
|
|
||||||
matches: [],
|
|
||||||
none: true,
|
|
||||||
sample: null,
|
|
||||||
total: allNodes.length,
|
|
||||||
};
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const matches = selectorMatching(allNodes, expressions, 'metadata.labels');
|
|
||||||
|
|
||||||
this.matchingResources = {
|
|
||||||
matched: matches.length,
|
|
||||||
matches,
|
|
||||||
none: matches.length === 0,
|
|
||||||
sample: matches[0]?.nameDisplay || null,
|
|
||||||
total: allNodes.length,
|
|
||||||
};
|
|
||||||
}, 100, { trailing: true })
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<MatchExpressions
|
|
||||||
v-model:value="selectorExpressions"
|
|
||||||
:mode="mode"
|
|
||||||
:show-remove="false"
|
|
||||||
:type="'node'"
|
|
||||||
:target-resources="allResourcesInScope"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<Banner :color="(matchingResources.none ? 'warning' : 'success')">
|
|
||||||
<span v-clean-html="t('generic.selectors.matchingResources.matchesSome', matchingResources)" />
|
|
||||||
</Banner>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<ResourceTable
|
|
||||||
:rows="matchingResources.matches"
|
|
||||||
:headers="tableHeaders"
|
|
||||||
key-field="id"
|
|
||||||
:table-actions="false"
|
|
||||||
:row-actions="false"
|
|
||||||
:schema="schema"
|
|
||||||
:groupable="false"
|
|
||||||
:search="false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -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">
|
|
||||||
<div class="upgrade-node-title">
|
|
||||||
<p>
|
<p>
|
||||||
{{ item.name }}
|
{{ item.name }} <span
|
||||||
</p>
|
|
||||||
<span
|
|
||||||
class="status"
|
class="status"
|
||||||
:class="{ [item.state]: true }"
|
:class="{ [item.state]: true }"
|
||||||
>
|
>{{ item.state }}</span>
|
||||||
{{ item.state }}
|
</p>
|
||||||
</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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -122,7 +122,7 @@ export default {
|
|||||||
return this.$store.getters['currentCluster'].isLocal;
|
return this.$store.getters['currentCluster'].isLocal;
|
||||||
},
|
},
|
||||||
canEditClusterMembers() {
|
canEditClusterMembers() {
|
||||||
return this.schema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
|
return this.normanClusterRTBSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -58,14 +58,11 @@ export default {
|
|||||||
|
|
||||||
const url = `https://${ host }${ prefix }/${ PRODUCT_NAME }/c/${ params.cluster }/console/${ uid }/${ type }`;
|
const url = `https://${ host }${ prefix }/${ PRODUCT_NAME }/c/${ params.cluster }/console/${ uid }/${ type }`;
|
||||||
|
|
||||||
// Defer so v-select can finish closing the dropdown before the popup steals focus
|
|
||||||
this.$nextTick(() => {
|
|
||||||
window.open(
|
window.open(
|
||||||
url,
|
url,
|
||||||
'_blank',
|
'_blank',
|
||||||
`toolbars=0,width=${ screen.width - 200 },height=${ screen.height - 200 },left=0,top=0,noreferrer`
|
`toolbars=0,width=${ screen.width - 200 },height=${ screen.height - 200 },left=0,top=0,noreferrer`
|
||||||
);
|
);
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
isEmpty(o) {
|
isEmpty(o) {
|
||||||
|
|||||||
@ -1,196 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import { NAMESPACE } from '@shell/config/types';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterClusterPodSecurityStandard',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
RadioGroup,
|
|
||||||
LabeledSelect,
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
await this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE });
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
let enabled = false;
|
|
||||||
let whitelistedNamespaces = [];
|
|
||||||
let privilegedNamespaces = [];
|
|
||||||
let restrictedNamespaces = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(this.value.value || this.value.default || '{}');
|
|
||||||
|
|
||||||
enabled = !!parsed.enabled;
|
|
||||||
whitelistedNamespaces = parsed.whitelistedNamespacesList ? parsed.whitelistedNamespacesList.split(',') : [];
|
|
||||||
privilegedNamespaces = parsed.privilegedNamespacesList ? parsed.privilegedNamespacesList.split(',') : [];
|
|
||||||
restrictedNamespaces = parsed.restrictedNamespacesList ? parsed.restrictedNamespacesList.split(',') : [];
|
|
||||||
} catch (e) {
|
|
||||||
enabled = false;
|
|
||||||
whitelistedNamespaces = [];
|
|
||||||
privilegedNamespaces = [];
|
|
||||||
restrictedNamespaces = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabled,
|
|
||||||
whitelistedNamespaces,
|
|
||||||
privilegedNamespaces,
|
|
||||||
restrictedNamespaces,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
enabledOptions() {
|
|
||||||
return [
|
|
||||||
{ label: this.t('generic.enabled'), value: true },
|
|
||||||
{ label: this.t('generic.disabled'), value: false },
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
allNamespaces() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
return this.$store.getters[`${ inStore }/all`](NAMESPACE).filter((ns) => !ns.isSystem).map((ns) => ns.id);
|
|
||||||
},
|
|
||||||
|
|
||||||
whitelistedOptions() {
|
|
||||||
const excluded = new Set([...this.privilegedNamespaces, ...this.restrictedNamespaces]);
|
|
||||||
|
|
||||||
return this.allNamespaces.filter((ns) => !excluded.has(ns));
|
|
||||||
},
|
|
||||||
|
|
||||||
privilegedOptions() {
|
|
||||||
const excluded = new Set([...this.whitelistedNamespaces, ...this.restrictedNamespaces]);
|
|
||||||
|
|
||||||
return this.allNamespaces.filter((ns) => !excluded.has(ns));
|
|
||||||
},
|
|
||||||
|
|
||||||
restrictedOptions() {
|
|
||||||
const excluded = new Set([...this.whitelistedNamespaces, ...this.privilegedNamespaces]);
|
|
||||||
|
|
||||||
return this.allNamespaces.filter((ns) => !excluded.has(ns));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
useDefault() {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(this.value.default || '{}');
|
|
||||||
|
|
||||||
this.enabled = !!parsed.enabled;
|
|
||||||
this.whitelistedNamespaces = parsed.whitelistedNamespacesList ? parsed.whitelistedNamespacesList.split(',') : [];
|
|
||||||
this.privilegedNamespaces = parsed.privilegedNamespacesList ? parsed.privilegedNamespacesList.split(',') : [];
|
|
||||||
this.restrictedNamespaces = parsed.restrictedNamespacesList ? parsed.restrictedNamespacesList.split(',') : [];
|
|
||||||
} catch (e) {
|
|
||||||
this.enabled = false;
|
|
||||||
this.whitelistedNamespaces = [];
|
|
||||||
this.privilegedNamespaces = [];
|
|
||||||
this.restrictedNamespaces = [];
|
|
||||||
}
|
|
||||||
this.save();
|
|
||||||
},
|
|
||||||
|
|
||||||
onUpdateEnabled() {
|
|
||||||
if (!this.enabled) {
|
|
||||||
this.whitelistedNamespaces = [];
|
|
||||||
this.privilegedNamespaces = [];
|
|
||||||
this.restrictedNamespaces = [];
|
|
||||||
}
|
|
||||||
this.save();
|
|
||||||
},
|
|
||||||
|
|
||||||
updateWhitelisted(selected) {
|
|
||||||
this.whitelistedNamespaces = selected;
|
|
||||||
this.save();
|
|
||||||
},
|
|
||||||
|
|
||||||
updatePrivileged(selected) {
|
|
||||||
this.privilegedNamespaces = selected;
|
|
||||||
this.save();
|
|
||||||
},
|
|
||||||
|
|
||||||
updateRestricted(selected) {
|
|
||||||
this.restrictedNamespaces = selected;
|
|
||||||
this.save();
|
|
||||||
},
|
|
||||||
|
|
||||||
save() {
|
|
||||||
this.value.value = JSON.stringify({
|
|
||||||
enabled: this.enabled,
|
|
||||||
whitelistedNamespacesList: this.whitelistedNamespaces.join(','),
|
|
||||||
privilegedNamespacesList: this.privilegedNamespaces.join(','),
|
|
||||||
restrictedNamespacesList: this.restrictedNamespaces.join(','),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<RadioGroup
|
|
||||||
v-model:value="enabled"
|
|
||||||
name="enabled"
|
|
||||||
:options="enabledOptions"
|
|
||||||
@update:value="onUpdateEnabled"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template v-if="enabled">
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="whitelistedNamespaces"
|
|
||||||
:label="t('harvester.setting.clusterPodSecurityStandard.whitelistedNamespaces.label')"
|
|
||||||
:options="whitelistedOptions"
|
|
||||||
:multiple="true"
|
|
||||||
:mode="mode"
|
|
||||||
@update:value="updateWhitelisted"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="privilegedNamespaces"
|
|
||||||
:label="t('harvester.setting.clusterPodSecurityStandard.privilegedNamespaces.label')"
|
|
||||||
:options="privilegedOptions"
|
|
||||||
:multiple="true"
|
|
||||||
:mode="mode"
|
|
||||||
@update:value="updatePrivileged"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="restrictedNamespaces"
|
|
||||||
:label="t('harvester.setting.clusterPodSecurityStandard.restrictedNamespaces.label')"
|
|
||||||
:options="restrictedOptions"
|
|
||||||
:multiple="true"
|
|
||||||
:mode="mode"
|
|
||||||
@update:value="updateRestricted"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</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,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>
|
|
||||||
@ -64,9 +64,6 @@ export default {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
parsedDefaultValue = JSON.parse(this.value.value);
|
parsedDefaultValue = JSON.parse(this.value.value);
|
||||||
if (typeof parsedDefaultValue.exclusiveVlan !== 'boolean') {
|
|
||||||
parsedDefaultValue.exclusiveVlan = false;
|
|
||||||
}
|
|
||||||
networkType = 'vlan' in parsedDefaultValue ? L2VLAN : UNTAGGED; // backend doesn't provide networkType, so we check if vlan is provided instead
|
networkType = 'vlan' in parsedDefaultValue ? L2VLAN : UNTAGGED; // backend doesn't provide networkType, so we check if vlan is provided instead
|
||||||
openVlan = true;
|
openVlan = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -75,8 +72,7 @@ export default {
|
|||||||
vlan: '',
|
vlan: '',
|
||||||
clusterNetwork: '',
|
clusterNetwork: '',
|
||||||
range: '',
|
range: '',
|
||||||
exclude: [],
|
exclude: []
|
||||||
exclusiveVlan: false
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const exclude = parsedDefaultValue?.exclude?.toString().split(',') || [];
|
const exclude = parsedDefaultValue?.exclude?.toString().split(',') || [];
|
||||||
@ -98,10 +94,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
showExclusiveVlan() {
|
|
||||||
return this.networkType === L2VLAN &&
|
|
||||||
Number(this.parsedDefaultValue.vlan) !== 1;
|
|
||||||
},
|
|
||||||
showVlan() {
|
showVlan() {
|
||||||
return this.networkType === L2VLAN;
|
return this.networkType === L2VLAN;
|
||||||
},
|
},
|
||||||
@ -140,18 +132,6 @@ export default {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
exclusiveVlanOptions() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: this.t('generic.enabled'),
|
|
||||||
value: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: this.t('generic.disabled'),
|
|
||||||
value: false
|
|
||||||
}
|
|
||||||
];
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@ -194,8 +174,7 @@ export default {
|
|||||||
vlan: '',
|
vlan: '',
|
||||||
clusterNetwork: '',
|
clusterNetwork: '',
|
||||||
range: '',
|
range: '',
|
||||||
exclude: [],
|
exclude: []
|
||||||
exclusiveVlan: false
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -301,15 +280,7 @@ export default {
|
|||||||
label-key="harvester.setting.storageNetwork.vlan"
|
label-key="harvester.setting.storageNetwork.vlan"
|
||||||
@update:value="inputVlan"
|
@update:value="inputVlan"
|
||||||
/>
|
/>
|
||||||
<LabeledSelect
|
|
||||||
v-if="showExclusiveVlan"
|
|
||||||
v-model:value="parsedDefaultValue.exclusiveVlan"
|
|
||||||
class="mb-20"
|
|
||||||
:options="exclusiveVlanOptions"
|
|
||||||
:mode="mode"
|
|
||||||
label-key="harvester.setting.storageNetwork.exclusiveVlan"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="parsedDefaultValue.clusterNetwork"
|
v-model:value="parsedDefaultValue.clusterNetwork"
|
||||||
label-key="harvester.setting.storageNetwork.clusterNetwork"
|
label-key="harvester.setting.storageNetwork.clusterNetwork"
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -54,26 +54,11 @@ const FEATURE_FLAGS = {
|
|||||||
'lhV2VolExpansion',
|
'lhV2VolExpansion',
|
||||||
'l2VlanTrunkMode',
|
'l2VlanTrunkMode',
|
||||||
'kubevirtMigration',
|
'kubevirtMigration',
|
||||||
'hotplugNic',
|
'hotplugNic'
|
||||||
'resumeUpgradePausedNode',
|
|
||||||
],
|
],
|
||||||
'v1.7.1': [],
|
'v1.7.1': [],
|
||||||
'v1.8.0': [
|
'v1.7.2': [
|
||||||
'hotplugCdRom',
|
'clusterRegistrationTLSVerify'
|
||||||
'supportBundleFileNameSetting',
|
|
||||||
'clusterRegistrationTLSVerify',
|
|
||||||
'vGPUAsPCIDevice',
|
|
||||||
'instanceManagerResourcesSetting',
|
|
||||||
'rwxNetworkSetting',
|
|
||||||
'createPVCWithDataVolume',
|
|
||||||
'clusterPodSecurityStandardSetting',
|
|
||||||
],
|
|
||||||
'v1.8.1': [],
|
|
||||||
'v1.9.0': [
|
|
||||||
'supportFilesystem',
|
|
||||||
'disableResourcePooling',
|
|
||||||
'expandOnlineEncryptedVolume',
|
|
||||||
'longhornV2HugepageSettings'
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -35,33 +35,12 @@ 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';
|
||||||
const LOGGING_GROUP = 'Monitoring & Logging::Logging';
|
const LOGGING_GROUP = 'Monitoring & Logging::Logging';
|
||||||
const OVERLAY_NETWORKS_GROUP = 'Overlay Networks';
|
|
||||||
const UNDERLAY_NETWORKS_GROUP = 'Underlay Networks';
|
|
||||||
const NAT_INTERNET_GROUP = `${ OVERLAY_NETWORKS_GROUP }::NAT & Internet`;
|
|
||||||
const GATEWAYS_GROUP = `${ NAT_INTERNET_GROUP }::Gateways`;
|
|
||||||
const EXTERNAL_IPS_GROUP = `${ NAT_INTERNET_GROUP }::External IPs`;
|
|
||||||
const RULES_GROUP = `${ NAT_INTERNET_GROUP }::Rules`;
|
|
||||||
const SOURCE_RULES_GROUP = `${ RULES_GROUP }::Source Rules`;
|
|
||||||
const DESTINATION_RULES_GROUP = `${ RULES_GROUP }::Destination Rules`;
|
|
||||||
|
|
||||||
export const PRODUCT_NAME = 'harvester';
|
export const PRODUCT_NAME = 'harvester';
|
||||||
|
|
||||||
@ -89,6 +68,7 @@ export function init($plugin, store) {
|
|||||||
configureType,
|
configureType,
|
||||||
virtualType,
|
virtualType,
|
||||||
weightGroup,
|
weightGroup,
|
||||||
|
weightType,
|
||||||
} = $plugin.DSL(store, PRODUCT_NAME);
|
} = $plugin.DSL(store, PRODUCT_NAME);
|
||||||
|
|
||||||
const isSingleVirtualCluster = process.env.rancherEnv === PRODUCT_NAME;
|
const isSingleVirtualCluster = process.env.rancherEnv === PRODUCT_NAME;
|
||||||
@ -175,7 +155,7 @@ export function init($plugin, store) {
|
|||||||
group: 'Root',
|
group: 'Root',
|
||||||
name: HCI.HOST,
|
name: HCI.HOST,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 499,
|
weight: 399,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.HOST }
|
params: { resource: HCI.HOST }
|
||||||
@ -207,7 +187,7 @@ export function init($plugin, store) {
|
|||||||
group: 'root',
|
group: 'root',
|
||||||
name: HCI.VM,
|
name: HCI.VM,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 498,
|
weight: 299,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.VM }
|
params: { resource: HCI.VM }
|
||||||
@ -215,146 +195,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,
|
|
||||||
ifHaveType: HCI.VMIMPORT,
|
|
||||||
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,
|
|
||||||
ifHaveType: HCI.VMIMPORT_SOURCE_V,
|
|
||||||
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,
|
|
||||||
ifHaveType: HCI.VMIMPORT_SOURCE_O,
|
|
||||||
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,
|
|
||||||
ifHaveType: HCI.VMIMPORT_SOURCE_OVA,
|
|
||||||
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: {
|
||||||
@ -372,7 +212,7 @@ export function init($plugin, store) {
|
|||||||
ifHaveType: PVC,
|
ifHaveType: PVC,
|
||||||
name: HCI.VOLUME,
|
name: HCI.VOLUME,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 497,
|
weight: 199,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.VOLUME }
|
params: { resource: HCI.VOLUME }
|
||||||
@ -398,7 +238,7 @@ export function init($plugin, store) {
|
|||||||
group: 'root',
|
group: 'root',
|
||||||
name: HCI.IMAGE,
|
name: HCI.IMAGE,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 496,
|
weight: 198,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.IMAGE }
|
params: { resource: HCI.IMAGE }
|
||||||
@ -413,7 +253,7 @@ export function init($plugin, store) {
|
|||||||
group: 'root',
|
group: 'root',
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
name: 'projects-namespaces',
|
name: 'projects-namespaces',
|
||||||
weight: 495,
|
weight: 98,
|
||||||
route: { name: `${ PRODUCT_NAME }-c-cluster-projectsnamespaces` },
|
route: { name: `${ PRODUCT_NAME }-c-cluster-projectsnamespaces` },
|
||||||
exact: true,
|
exact: true,
|
||||||
});
|
});
|
||||||
@ -425,7 +265,7 @@ export function init($plugin, store) {
|
|||||||
labelKey: 'harvester.namespace.label',
|
labelKey: 'harvester.namespace.label',
|
||||||
name: NAMESPACE,
|
name: NAMESPACE,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 495,
|
weight: 89,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: NAMESPACE }
|
params: { resource: NAMESPACE }
|
||||||
@ -496,7 +336,6 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
ifHaveType: LOGGING.CLUSTER_FLOW,
|
|
||||||
labelKey: 'harvester.logging.clusterFlow.label',
|
labelKey: 'harvester.logging.clusterFlow.label',
|
||||||
name: HCI.CLUSTER_FLOW,
|
name: HCI.CLUSTER_FLOW,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
@ -520,7 +359,6 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
ifHaveType: LOGGING.CLUSTER_OUTPUT,
|
|
||||||
labelKey: 'harvester.logging.clusterOutput.label',
|
labelKey: 'harvester.logging.clusterOutput.label',
|
||||||
name: HCI.CLUSTER_OUTPUT,
|
name: HCI.CLUSTER_OUTPUT,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
@ -544,7 +382,6 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
ifHaveType: LOGGING.FLOW,
|
|
||||||
labelKey: 'harvester.logging.flow.label',
|
labelKey: 'harvester.logging.flow.label',
|
||||||
name: HCI.FLOW,
|
name: HCI.FLOW,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
@ -568,7 +405,6 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
ifHaveType: LOGGING.OUTPUT,
|
|
||||||
labelKey: 'harvester.logging.output.label',
|
labelKey: 'harvester.logging.output.label',
|
||||||
name: HCI.OUTPUT,
|
name: HCI.OUTPUT,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
@ -589,53 +425,14 @@ export function init($plugin, store) {
|
|||||||
[
|
[
|
||||||
HCI.CLUSTER_NETWORK,
|
HCI.CLUSTER_NETWORK,
|
||||||
HCI.NETWORK_ATTACHMENT,
|
HCI.NETWORK_ATTACHMENT,
|
||||||
HCI.HOST_NETWORK_CONFIG,
|
HCI.VPC,
|
||||||
|
NETWORK_POLICY,
|
||||||
HCI.LB,
|
HCI.LB,
|
||||||
HCI.IP_POOL,
|
HCI.IP_POOL,
|
||||||
],
|
],
|
||||||
'networks'
|
'networks'
|
||||||
);
|
);
|
||||||
|
|
||||||
basicType(
|
|
||||||
[HCI.VPC],
|
|
||||||
OVERLAY_NETWORKS_GROUP
|
|
||||||
);
|
|
||||||
|
|
||||||
basicType(
|
|
||||||
[NETWORK_POLICY],
|
|
||||||
OVERLAY_NETWORKS_GROUP
|
|
||||||
);
|
|
||||||
|
|
||||||
basicType(
|
|
||||||
[HCI.VPC_NAT_GATEWAY],
|
|
||||||
GATEWAYS_GROUP
|
|
||||||
);
|
|
||||||
|
|
||||||
basicType(
|
|
||||||
[HCI.IPTABLES_EIP],
|
|
||||||
EXTERNAL_IPS_GROUP
|
|
||||||
);
|
|
||||||
|
|
||||||
basicType(
|
|
||||||
[HCI.IPTABLES_SNAT_RULE],
|
|
||||||
SOURCE_RULES_GROUP
|
|
||||||
);
|
|
||||||
|
|
||||||
basicType(
|
|
||||||
[HCI.IPTABLES_DNAT_RULE],
|
|
||||||
DESTINATION_RULES_GROUP
|
|
||||||
);
|
|
||||||
|
|
||||||
basicType(
|
|
||||||
[HCI.PROVIDER_NETWORK],
|
|
||||||
UNDERLAY_NETWORKS_GROUP
|
|
||||||
);
|
|
||||||
|
|
||||||
basicType(
|
|
||||||
[HCI.VLAN],
|
|
||||||
UNDERLAY_NETWORKS_GROUP
|
|
||||||
);
|
|
||||||
|
|
||||||
basicType(
|
basicType(
|
||||||
[
|
[
|
||||||
HCI.SCHEDULE_VM_BACKUP,
|
HCI.SCHEDULE_VM_BACKUP,
|
||||||
@ -646,12 +443,9 @@ export function init($plugin, store) {
|
|||||||
'backupAndSnapshot'
|
'backupAndSnapshot'
|
||||||
);
|
);
|
||||||
|
|
||||||
weightGroup('networks', 494, true);
|
weightGroup('networks', 300, true);
|
||||||
weightGroup('Overlay Networks', 493, true);
|
weightType(NAMESPACE, 299, true);
|
||||||
weightGroup('NAT & Internet', 492, true);
|
weightGroup('backupAndSnapshot', 289, true);
|
||||||
weightGroup('Rules', 491, true);
|
|
||||||
weightGroup('Underlay Networks', 490, true);
|
|
||||||
weightGroup('backupAndSnapshot', 489, true);
|
|
||||||
|
|
||||||
basicType(
|
basicType(
|
||||||
[
|
[
|
||||||
@ -730,7 +524,7 @@ export function init($plugin, store) {
|
|||||||
name: HCI.CLUSTER_NETWORK,
|
name: HCI.CLUSTER_NETWORK,
|
||||||
ifHaveType: HCI.CLUSTER_NETWORK,
|
ifHaveType: HCI.CLUSTER_NETWORK,
|
||||||
namespaced: false,
|
namespaced: false,
|
||||||
weight: 484,
|
weight: 189,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.CLUSTER_NETWORK }
|
params: { resource: HCI.CLUSTER_NETWORK }
|
||||||
@ -745,14 +539,14 @@ export function init($plugin, store) {
|
|||||||
},
|
},
|
||||||
resource: NETWORK_ATTACHMENT,
|
resource: NETWORK_ATTACHMENT,
|
||||||
resourceDetail: HCI.NETWORK_ATTACHMENT,
|
resourceDetail: HCI.NETWORK_ATTACHMENT,
|
||||||
resourceEdit: HCI.NETWORK_ATTACHMENT,
|
resourceEdit: HCI.NETWORK_ATTACHMENT
|
||||||
});
|
});
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
labelKey: 'harvester.network.label',
|
labelKey: 'harvester.network.label',
|
||||||
name: HCI.NETWORK_ATTACHMENT,
|
name: HCI.NETWORK_ATTACHMENT,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 485,
|
weight: 188,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.NETWORK_ATTACHMENT }
|
params: { resource: HCI.NETWORK_ATTACHMENT }
|
||||||
@ -766,7 +560,7 @@ export function init($plugin, store) {
|
|||||||
labelKey: 'harvester.vpc.label',
|
labelKey: 'harvester.vpc.label',
|
||||||
name: HCI.VPC,
|
name: HCI.VPC,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 195,
|
weight: 187,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.VPC }
|
params: { resource: HCI.VPC }
|
||||||
@ -775,73 +569,13 @@ export function init($plugin, store) {
|
|||||||
ifHaveType: HCI.VPC,
|
ifHaveType: HCI.VPC,
|
||||||
});
|
});
|
||||||
|
|
||||||
configureType(HCI.VPC_NAT_GATEWAY, { hiddenNamespaceGroupButton: true, canYaml: false });
|
|
||||||
|
|
||||||
virtualType({
|
|
||||||
labelKey: 'harvester.natGateway.label',
|
|
||||||
name: HCI.VPC_NAT_GATEWAY,
|
|
||||||
namespaced: false,
|
|
||||||
weight: 193,
|
|
||||||
route: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.VPC_NAT_GATEWAY }
|
|
||||||
},
|
|
||||||
exact: false,
|
|
||||||
ifHaveType: HCI.VPC_NAT_GATEWAY,
|
|
||||||
});
|
|
||||||
|
|
||||||
configureType(HCI.IPTABLES_EIP, { hiddenNamespaceGroupButton: true, canYaml: false });
|
|
||||||
|
|
||||||
virtualType({
|
|
||||||
labelKey: 'harvester.externalIP.label',
|
|
||||||
name: HCI.IPTABLES_EIP,
|
|
||||||
namespaced: false,
|
|
||||||
weight: 192,
|
|
||||||
route: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.IPTABLES_EIP }
|
|
||||||
},
|
|
||||||
exact: false,
|
|
||||||
ifHaveType: HCI.IPTABLES_EIP,
|
|
||||||
});
|
|
||||||
|
|
||||||
configureType(HCI.IPTABLES_SNAT_RULE, { hiddenNamespaceGroupButton: true, canYaml: false });
|
|
||||||
|
|
||||||
virtualType({
|
|
||||||
labelKey: 'harvester.snat.label',
|
|
||||||
name: HCI.IPTABLES_SNAT_RULE,
|
|
||||||
namespaced: false,
|
|
||||||
weight: 191,
|
|
||||||
route: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.IPTABLES_SNAT_RULE }
|
|
||||||
},
|
|
||||||
exact: false,
|
|
||||||
ifHaveType: HCI.IPTABLES_SNAT_RULE,
|
|
||||||
});
|
|
||||||
|
|
||||||
configureType(HCI.IPTABLES_DNAT_RULE, { hiddenNamespaceGroupButton: true, canYaml: false });
|
|
||||||
|
|
||||||
virtualType({
|
|
||||||
labelKey: 'harvester.dnat.label',
|
|
||||||
name: HCI.IPTABLES_DNAT_RULE,
|
|
||||||
namespaced: false,
|
|
||||||
weight: 190,
|
|
||||||
route: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.IPTABLES_DNAT_RULE }
|
|
||||||
},
|
|
||||||
exact: false,
|
|
||||||
ifHaveType: HCI.IPTABLES_DNAT_RULE,
|
|
||||||
});
|
|
||||||
|
|
||||||
configureType(NETWORK_POLICY, { hiddenNamespaceGroupButton: true, canYaml: false });
|
configureType(NETWORK_POLICY, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
labelKey: 'harvester.networkPolicy.label',
|
labelKey: 'harvester.networkPolicy.label',
|
||||||
name: NETWORK_POLICY,
|
name: NETWORK_POLICY,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 194,
|
weight: 186,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: NETWORK_POLICY }
|
params: { resource: NETWORK_POLICY }
|
||||||
@ -850,53 +584,6 @@ export function init($plugin, store) {
|
|||||||
ifHaveType: NETWORK_POLICY,
|
ifHaveType: NETWORK_POLICY,
|
||||||
});
|
});
|
||||||
|
|
||||||
configureType(HCI.PROVIDER_NETWORK, { hiddenNamespaceGroupButton: true, canYaml: false });
|
|
||||||
|
|
||||||
virtualType({
|
|
||||||
labelKey: 'harvester.providerNetwork.label',
|
|
||||||
name: HCI.PROVIDER_NETWORK,
|
|
||||||
namespaced: false,
|
|
||||||
weight: 189,
|
|
||||||
route: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.PROVIDER_NETWORK }
|
|
||||||
},
|
|
||||||
exact: false,
|
|
||||||
ifHaveType: HCI.PROVIDER_NETWORK,
|
|
||||||
});
|
|
||||||
|
|
||||||
configureType(HCI.VLAN, { hiddenNamespaceGroupButton: true, canYaml: false });
|
|
||||||
|
|
||||||
headers(HCI.VLAN, [
|
|
||||||
STATE,
|
|
||||||
NAME_COL,
|
|
||||||
{
|
|
||||||
name: 'id',
|
|
||||||
label: 'ID',
|
|
||||||
value: 'spec.id',
|
|
||||||
sort: 'spec.id'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'provider',
|
|
||||||
labelKey: 'harvester.subnet.provider.label',
|
|
||||||
value: 'spec.provider',
|
|
||||||
sort: 'spec.provider'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
virtualType({
|
|
||||||
labelKey: 'harvester.vlanNetwork.label',
|
|
||||||
name: HCI.VLAN,
|
|
||||||
namespaced: false,
|
|
||||||
weight: 188,
|
|
||||||
route: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.VLAN }
|
|
||||||
},
|
|
||||||
exact: false,
|
|
||||||
ifHaveType: HCI.VLAN,
|
|
||||||
});
|
|
||||||
|
|
||||||
configureType(HCI.SNAPSHOT, {
|
configureType(HCI.SNAPSHOT, {
|
||||||
isCreatable: false,
|
isCreatable: false,
|
||||||
location: {
|
location: {
|
||||||
@ -1251,7 +938,7 @@ export function init($plugin, store) {
|
|||||||
labelKey: 'harvester.loadBalancer.label',
|
labelKey: 'harvester.loadBalancer.label',
|
||||||
name: HCI.LB,
|
name: HCI.LB,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 483,
|
weight: 185,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.LB }
|
params: { resource: HCI.LB }
|
||||||
@ -1290,7 +977,7 @@ export function init($plugin, store) {
|
|||||||
labelKey: 'harvester.ipPool.label',
|
labelKey: 'harvester.ipPool.label',
|
||||||
name: HCI.IP_POOL,
|
name: HCI.IP_POOL,
|
||||||
namespaced: false,
|
namespaced: false,
|
||||||
weight: 482,
|
weight: 184,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.IP_POOL }
|
params: { resource: HCI.IP_POOL }
|
||||||
@ -1299,24 +986,4 @@ export function init($plugin, store) {
|
|||||||
ifHaveType: HCI.IP_POOL,
|
ifHaveType: HCI.IP_POOL,
|
||||||
});
|
});
|
||||||
headers(HCI.IP_POOL, IP_POOL_HEADERS);
|
headers(HCI.IP_POOL, IP_POOL_HEADERS);
|
||||||
|
|
||||||
configureType(HCI.HOST_NETWORK_CONFIG, {
|
|
||||||
location: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.HOST_NETWORK_CONFIG }
|
|
||||||
},
|
|
||||||
canYaml: false,
|
|
||||||
});
|
|
||||||
virtualType({
|
|
||||||
labelKey: 'harvester.hostNetworkConfig.label',
|
|
||||||
name: HCI.HOST_NETWORK_CONFIG,
|
|
||||||
namespaced: false,
|
|
||||||
weight: 481,
|
|
||||||
route: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.HOST_NETWORK_CONFIG }
|
|
||||||
},
|
|
||||||
exact: false,
|
|
||||||
ifHaveType: HCI.HOST_NETWORK_CONFIG,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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'
|
||||||
@ -88,8 +82,6 @@ export const CSI_SECRETS = {
|
|||||||
CSI_NODE_PUBLISH_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-publish-secret-namespace',
|
CSI_NODE_PUBLISH_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-publish-secret-namespace',
|
||||||
CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name',
|
CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name',
|
||||||
CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace',
|
CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace',
|
||||||
CSI_NODE_EXPAND_SECRET_NAME: 'csi.storage.k8s.io/node-expand-secret-name',
|
|
||||||
CSI_NODE_EXPAND_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-expand-secret-namespace'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Some harvester CRD type is not equal to model file name, define the mapping here
|
// Some harvester CRD type is not equal to model file name, define the mapping here
|
||||||
|
|||||||
@ -19,7 +19,6 @@ export const HCI = {
|
|||||||
NETWORK_TYPE: 'network.harvesterhci.io/type',
|
NETWORK_TYPE: 'network.harvesterhci.io/type',
|
||||||
VM_NAME: 'harvesterhci.io/vmName',
|
VM_NAME: 'harvesterhci.io/vmName',
|
||||||
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
|
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
|
||||||
VM_DISPLAY_NAME: 'harvesterhci.io/vmDisplayName',
|
|
||||||
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
|
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
|
||||||
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
|
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
|
||||||
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
|
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
|
||||||
@ -29,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',
|
||||||
@ -80,7 +78,4 @@ export const HCI = {
|
|||||||
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',
|
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',
|
|
||||||
CNI_NETWORKS: 'k8s.v1.cni.cncf.io/networks',
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,11 +16,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',
|
||||||
@ -35,16 +33,12 @@ export const HCI_SETTING = {
|
|||||||
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
|
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
|
||||||
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
|
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
|
||||||
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
|
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
|
||||||
LONGHORN_V2_DATA_ENGINE_HUGEPAGE_ENABLED: 'longhorn-v2-data-engine-hugepage-enabled',
|
|
||||||
LONGHORN_V2_DATA_ENGINE_MEMORY_SIZE: 'longhorn-v2-data-engine-memory-size',
|
|
||||||
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
|
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
|
||||||
UPGRADE_CONFIG: 'upgrade-config',
|
UPGRADE_CONFIG: 'upgrade-config',
|
||||||
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
||||||
RANCHER_CLUSTER: 'rancher-cluster',
|
RANCHER_CLUSTER: 'rancher-cluster',
|
||||||
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
|
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
|
||||||
KUBEVIRT_MIGRATION: 'kubevirt-migration',
|
KUBEVIRT_MIGRATION: 'kubevirt-migration'
|
||||||
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources',
|
|
||||||
CLUSTER_POD_SECURITY_STANDARD: 'cluster-pod-security-standard'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_ALLOWED_SETTINGS = {
|
export const HCI_ALLOWED_SETTINGS = {
|
||||||
@ -77,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]: {
|
||||||
@ -114,14 +102,6 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
experimental: true,
|
experimental: true,
|
||||||
featureFlag: 'longhornV2LVMSupport'
|
featureFlag: 'longhornV2LVMSupport'
|
||||||
},
|
},
|
||||||
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_HUGEPAGE_ENABLED]: {
|
|
||||||
kind: 'boolean',
|
|
||||||
featureFlag: 'longhornV2HugepageSettings'
|
|
||||||
},
|
|
||||||
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_MEMORY_SIZE]: {
|
|
||||||
kind: 'number',
|
|
||||||
featureFlag: 'longhornV2HugepageSettings'
|
|
||||||
},
|
|
||||||
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
|
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
|
||||||
[HCI_SETTING.UPGRADE_CONFIG]: {
|
[HCI_SETTING.UPGRADE_CONFIG]: {
|
||||||
kind: 'json',
|
kind: 'json',
|
||||||
@ -138,12 +118,6 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
},
|
},
|
||||||
[HCI_SETTING.KUBEVIRT_MIGRATION]: {
|
[HCI_SETTING.KUBEVIRT_MIGRATION]: {
|
||||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'kubevirtMigration',
|
kind: 'json', from: 'import', canReset: true, featureFlag: 'kubevirtMigration',
|
||||||
},
|
|
||||||
[HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: {
|
|
||||||
kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting'
|
|
||||||
},
|
|
||||||
[HCI_SETTING.CLUSTER_POD_SECURITY_STANDARD]: {
|
|
||||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'clusterPodSecurityStandardSetting'
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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',
|
|
||||||
};
|
|
||||||
|
|||||||
@ -29,26 +29,3 @@ export const L2VLAN_MODE = {
|
|||||||
ACCESS: 'access',
|
ACCESS: 'access',
|
||||||
TRUNK: 'trunk',
|
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',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FILESYSTEM_SOURCE_TYPE = {
|
|
||||||
CONFIGMAP: 'configmap',
|
|
||||||
SECRET: 'secret',
|
|
||||||
SERVICEACCOUNT: 'serviceaccount',
|
|
||||||
};
|
|
||||||
|
|||||||
@ -27,11 +27,15 @@ export default {
|
|||||||
await allHash({
|
await allHash({
|
||||||
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
|
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
|
||||||
vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }),
|
vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }),
|
||||||
vmims: this.$store.dispatch('harvester/findAll', { type: HCI.VMIM }),
|
allClusterNetwork: this.$store.dispatch('harvester/findAll', { type: HCI.CLUSTER_NETWORK }),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
allClusterNetwork() {
|
||||||
|
return this.$store.getters['harvester/all'](HCI.CLUSTER_NETWORK);
|
||||||
|
},
|
||||||
|
|
||||||
rows() {
|
rows() {
|
||||||
const vms = this.$store.getters['harvester/all'](HCI.VM);
|
const vms = this.$store.getters['harvester/all'](HCI.VM);
|
||||||
|
|
||||||
@ -104,6 +108,7 @@ export default {
|
|||||||
<HarvesterVmState
|
<HarvesterVmState
|
||||||
class="vmstate"
|
class="vmstate"
|
||||||
:row="scope.row"
|
:row="scope.row"
|
||||||
|
:all-cluster-network="allClusterNetwork"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -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
@ -98,9 +98,9 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
|
|
||||||
close(data) {
|
close() {
|
||||||
this.errors = [];
|
this.errors = [];
|
||||||
this.$emit('close', data);
|
this.$emit('close');
|
||||||
},
|
},
|
||||||
|
|
||||||
async apply(buttonDone) {
|
async apply(buttonDone) {
|
||||||
@ -109,7 +109,7 @@ export default {
|
|||||||
await resource.doActionGrowl(this.modalData.action, {});
|
await resource.doActionGrowl(this.modalData.action, {});
|
||||||
}
|
}
|
||||||
buttonDone(true);
|
buttonDone(true);
|
||||||
this.close({ performCallback: true, clearTableSelection: true });
|
this.close();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errors = exceptionToErrorsArray(e);
|
this.errors = exceptionToErrorsArray(e);
|
||||||
buttonDone(false);
|
buttonDone(false);
|
||||||
|
|||||||
@ -1,12 +1,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import { Card } from '@components/Card';
|
import { Card } from '@components/Card';
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
|
||||||
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',
|
||||||
@ -15,9 +12,7 @@ export default {
|
|||||||
|
|
||||||
components: {
|
components: {
|
||||||
AsyncButton,
|
AsyncButton,
|
||||||
Banner,
|
|
||||||
Card,
|
Card,
|
||||||
Checkbox,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@ -28,16 +23,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return { disableResourcePooling: false };
|
return {};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: { ...mapGetters({ t: 'i18n/t' }) },
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
disableResourcePoolingEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('disableResourcePooling');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
close() {
|
close() {
|
||||||
@ -45,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];
|
||||||
@ -64,8 +62,7 @@ export default {
|
|||||||
spec: {
|
spec: {
|
||||||
address: actionResource.status.address,
|
address: actionResource.status.address,
|
||||||
nodeName: actionResource.status.nodeName,
|
nodeName: actionResource.status.nodeName,
|
||||||
userName,
|
userName
|
||||||
disableResourcePooling: this.disableResourcePooling,
|
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
@ -96,19 +93,7 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body>
|
<template #body>
|
||||||
<p class="mb-20">
|
|
||||||
{{ t('harvester.pci.enablePassthroughWarning') }}
|
{{ t('harvester.pci.enablePassthroughWarning') }}
|
||||||
</p>
|
|
||||||
<template v-if="disableResourcePoolingEnabled">
|
|
||||||
<Checkbox
|
|
||||||
v-model:value="disableResourcePooling"
|
|
||||||
label-key="harvester.pci.disableResourcePooling"
|
|
||||||
/>
|
|
||||||
<Banner
|
|
||||||
color="info"
|
|
||||||
:label="t('harvester.pci.disableResourcePoolingDescription')"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
|
|||||||
@ -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];
|
||||||
|
|||||||
@ -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,154 +0,0 @@
|
|||||||
<script>
|
|
||||||
import merge from 'lodash/merge';
|
|
||||||
import jsyaml from 'js-yaml';
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { Card } from '@components/Card';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
|
||||||
import { escapeHtml } from '@shell/utils/string';
|
|
||||||
|
|
||||||
const DEFAULT_VALUE = { image: { repository: 'rancher/harvester-nvidia-driver-toolkit' } };
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterEnableNvidiaDriverToolkit',
|
|
||||||
|
|
||||||
emits: ['close'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
AsyncButton,
|
|
||||||
Card,
|
|
||||||
LabeledInput,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
resources: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
const addon = this.resources[0];
|
|
||||||
let valuesContentJson;
|
|
||||||
|
|
||||||
try {
|
|
||||||
valuesContentJson = merge({}, DEFAULT_VALUE, jsyaml.load(addon.spec.valuesContent));
|
|
||||||
} catch (e) {
|
|
||||||
valuesContentJson = { ...DEFAULT_VALUE };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { valuesContentJson };
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
buttonDisabled() {
|
|
||||||
const { image, driverLocation } = this.valuesContentJson;
|
|
||||||
|
|
||||||
return !(image?.repository || '').trim() || !(image?.tag || '').trim() || !(driverLocation || '').trim();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.$emit('close');
|
|
||||||
},
|
|
||||||
|
|
||||||
async enable(buttonCb) {
|
|
||||||
const addon = this.resources[0];
|
|
||||||
|
|
||||||
try {
|
|
||||||
addon.spec.valuesContent = jsyaml.dump(this.valuesContentJson);
|
|
||||||
addon.spec.enabled = true;
|
|
||||||
await addon.save();
|
|
||||||
buttonCb(true);
|
|
||||||
this.close();
|
|
||||||
} catch (err) {
|
|
||||||
addon.spec.enabled = false;
|
|
||||||
this.$store.dispatch('growl/fromError', {
|
|
||||||
title: this.t('generic.notification.title.error', { name: escapeHtml(addon.metadata.name) }),
|
|
||||||
err,
|
|
||||||
}, { root: true });
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Card :show-highlight-border="false">
|
|
||||||
<template #title>
|
|
||||||
<h4
|
|
||||||
v-clean-html="t('harvester.addons.nvidiaDriverToolkit.enable.title')"
|
|
||||||
class="text-default-text"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<div class="body">
|
|
||||||
<div class="row mb-15">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="valuesContentJson.image.repository"
|
|
||||||
:required="true"
|
|
||||||
:label="t('harvester.addons.nvidiaDriverToolkit.image.repository')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="valuesContentJson.image.tag"
|
|
||||||
:required="true"
|
|
||||||
:label="t('harvester.addons.nvidiaDriverToolkit.image.tag')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-15">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="valuesContentJson.driverLocation"
|
|
||||||
:required="true"
|
|
||||||
:label="t('harvester.addons.nvidiaDriverToolkit.driver.location')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #actions>
|
|
||||||
<div class="buttons actions">
|
|
||||||
<button
|
|
||||||
class="btn role-secondary mr-10"
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
{{ t('generic.cancel') }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<AsyncButton
|
|
||||||
mode="enable"
|
|
||||||
:disabled="buttonDisabled"
|
|
||||||
@click="enable"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -5,10 +5,6 @@ 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: 'HarvesterHotUnplug',
|
||||||
|
|
||||||
@ -44,37 +40,19 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
isVolume() {
|
isVolume() {
|
||||||
return this.modalData.type === VOLUME;
|
return this.modalData.type === 'volume';
|
||||||
},
|
},
|
||||||
|
|
||||||
titleKey() {
|
titleKey() {
|
||||||
const keys = {
|
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.title' : 'harvester.virtualMachine.hotUnplug.detachNIC.title';
|
||||||
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.title',
|
|
||||||
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.title',
|
|
||||||
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.title',
|
|
||||||
};
|
|
||||||
|
|
||||||
return keys[this.modalData.type];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
actionLabelKey() {
|
actionLabelKey() {
|
||||||
const keys = {
|
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabel' : 'harvester.virtualMachine.hotUnplug.detachNIC.actionLabel';
|
||||||
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabels',
|
|
||||||
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.actionLabels',
|
|
||||||
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.actionLabels',
|
|
||||||
};
|
|
||||||
|
|
||||||
return keys[this.modalData.type];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
successMessageKey() {
|
successMessageKey() {
|
||||||
const keys = {
|
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.success' : 'harvester.virtualMachine.hotUnplug.detachNIC.success';
|
||||||
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.success',
|
|
||||||
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.success',
|
|
||||||
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.success',
|
|
||||||
};
|
|
||||||
|
|
||||||
return keys[this.modalData.type];
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -87,12 +65,10 @@ export default {
|
|||||||
try {
|
try {
|
||||||
let res;
|
let res;
|
||||||
|
|
||||||
if (this.modalData.type === VOLUME) {
|
if (this.isVolume) {
|
||||||
res = await this.actionResource.doAction('removeVolume', { diskName: this.name });
|
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 {
|
} else {
|
||||||
res = await this.actionResource.doAction('ejectCdRomVolume', { deviceName: this.name });
|
res = await this.actionResource.doAction('removeNic', { interfaceName: this.name });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res._status === 200 || res._status === 204) {
|
if (res._status === 200 || res._status === 204) {
|
||||||
|
|||||||
@ -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,12 +1,15 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
import { NODE } from '@shell/config/types';
|
import { NODE } from '@shell/config/types';
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
|
||||||
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';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
@ -59,46 +62,28 @@ export default {
|
|||||||
return this.resources[0];
|
return this.resources[0];
|
||||||
},
|
},
|
||||||
|
|
||||||
anyCpuPinning() {
|
vmi() {
|
||||||
return this.resources.some((r) => r.isCpuPinning);
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
},
|
const vmiResources = this.$store.getters[`${ inStore }/all`](HCI.VMI);
|
||||||
|
const resource = vmiResources.find((VMI) => VMI.id === this.actionResource?.id) || null;
|
||||||
|
|
||||||
vmsByNode() {
|
return resource;
|
||||||
const groups = {};
|
|
||||||
|
|
||||||
for (const r of this.resources) {
|
|
||||||
const node = r.nodeName || '';
|
|
||||||
const name = r.nameDisplay || r.name || r.id;
|
|
||||||
|
|
||||||
if (!groups[node]) {
|
|
||||||
groups[node] = [];
|
|
||||||
}
|
|
||||||
groups[node].push(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.entries(groups).map(([node, vms]) => ({ node, vms })).sort((a, b) => a.node.localeCompare(b.node));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
cpuPinningAlertMessage() {
|
cpuPinningAlertMessage() {
|
||||||
return this.t('harvester.virtualMachine.cpuPinning.migrationMessage');
|
return this.t('harvester.virtualMachine.cpuPinning.migrationMessage');
|
||||||
},
|
},
|
||||||
|
|
||||||
allVmsOnTargetNode() {
|
|
||||||
if (!this.nodeName) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.resources.every((r) => r.nodeName === this.nodeName);
|
|
||||||
},
|
|
||||||
|
|
||||||
nodeNameList() {
|
nodeNameList() {
|
||||||
const nodes = this.$store.getters['harvester/all'](NODE);
|
const nodes = this.$store.getters['harvester/all'](NODE);
|
||||||
|
|
||||||
return nodes.filter((n) => {
|
return nodes.filter((n) => {
|
||||||
|
const isNotSelfNode = !!this.availableNodes.includes(n.id);
|
||||||
const isNotWitnessNode = n.isEtcd !== 'true'; // do not allow to migrate to self node and witness node
|
const isNotWitnessNode = n.isEtcd !== 'true'; // do not allow to migrate to self node and witness node
|
||||||
const matchingCpuManagerConfig = !this.anyCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
|
const isCpuPinning = this.actionResource?.isCpuPinning;
|
||||||
|
const matchingCpuManagerConfig = !isCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
|
||||||
|
|
||||||
return isNotWitnessNode && matchingCpuManagerConfig;
|
return isNotSelfNode && isNotWitnessNode && matchingCpuManagerConfig;
|
||||||
}).map((n) => {
|
}).map((n) => {
|
||||||
let label = n?.metadata?.name;
|
let label = n?.metadata?.name;
|
||||||
const value = n?.metadata?.name;
|
const value = n?.metadata?.name;
|
||||||
@ -117,10 +102,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
close(data) {
|
close() {
|
||||||
this.nodeName = '';
|
this.nodeName = '';
|
||||||
this.errors = [];
|
this.errors = [];
|
||||||
this.$emit('close', data);
|
this.$emit('close');
|
||||||
},
|
},
|
||||||
|
|
||||||
async apply(buttonDone) {
|
async apply(buttonDone) {
|
||||||
@ -141,32 +126,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Filter out VMs already running on the selected node
|
await this.actionResource.doAction('migrate', { nodeName: this.nodeName }, {}, false);
|
||||||
const toMigrate = this.resources.filter((r) => r.nodeName !== this.nodeName);
|
|
||||||
|
|
||||||
// await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
|
|
||||||
// We want to show all migration errors if there are multiple VMs, so we use allSettled here and handle the results accordingly.
|
|
||||||
const results = await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
|
|
||||||
|
|
||||||
const failedMigrations = results
|
|
||||||
.map((result, index) => ({ resource: toMigrate[index], result }))
|
|
||||||
.filter(({ result }) => result.status === 'rejected');
|
|
||||||
|
|
||||||
if (failedMigrations.length) {
|
|
||||||
this['errors'] = failedMigrations.flatMap(({ resource, result }) => {
|
|
||||||
const vmName = resource?.nameDisplay || resource?.name || resource?.metadata?.name || this.$store.getters['i18n/t']('generic.unknown');
|
|
||||||
const error = result.reason?.data || result.reason;
|
|
||||||
const messages = exceptionToErrorsArray(error);
|
|
||||||
|
|
||||||
return messages.map((message) => `${ vmName }: ${ message }`);
|
|
||||||
});
|
|
||||||
buttonDone(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
buttonDone(true);
|
buttonDone(true);
|
||||||
this.close({ performCallback: true, clearTableSelection: true });
|
this.close();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err?.data || err;
|
const error = err?.data || err;
|
||||||
const message = exceptionToErrorsArray(error);
|
const message = exceptionToErrorsArray(error);
|
||||||
@ -183,35 +146,17 @@ export default {
|
|||||||
<template>
|
<template>
|
||||||
<Card :show-highlight-border="false">
|
<Card :show-highlight-border="false">
|
||||||
<template #title>
|
<template #title>
|
||||||
{{ t('harvester.modal.migration.vmMigrationTitle', { count: resources.length }) }}
|
{{ t('harvester.modal.migration.title') }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body>
|
<template #body>
|
||||||
<Banner
|
<Banner
|
||||||
v-if="anyCpuPinning"
|
v-if="actionResource?.isCpuPinning"
|
||||||
color="warning"
|
color="warning"
|
||||||
:label="cpuPinningAlertMessage"
|
:label="cpuPinningAlertMessage"
|
||||||
/>
|
/>
|
||||||
<p>
|
|
||||||
{{ t('harvester.modal.migration.selectedVMs') }}
|
|
||||||
</p>
|
|
||||||
<ul class="vm-list">
|
|
||||||
<li
|
|
||||||
v-for="group in vmsByNode"
|
|
||||||
:key="group.node"
|
|
||||||
>
|
|
||||||
{{ group.node || t('harvester.modal.migration.unknownNode') }}: {{ group.vms.join(', ') }}
|
|
||||||
<span
|
|
||||||
v-if="nodeName && group.node === nodeName"
|
|
||||||
class="already-on-target"
|
|
||||||
>
|
|
||||||
({{ t('harvester.modal.migration.alreadyOnTarget') }})
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="nodeName"
|
v-model:value="nodeName"
|
||||||
class="mt-15"
|
|
||||||
:label="t('harvester.modal.migration.fields.nodeName.label')"
|
:label="t('harvester.modal.migration.fields.nodeName.label')"
|
||||||
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
|
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
|
||||||
:options="nodeNameList"
|
:options="nodeNameList"
|
||||||
@ -238,7 +183,7 @@ export default {
|
|||||||
|
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
mode="apply"
|
mode="apply"
|
||||||
:disabled="!nodeName || allVmsOnTargetNode"
|
:disabled="!nodeName"
|
||||||
@click="apply"
|
@click="apply"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -256,16 +201,4 @@ export default {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.already-on-target {
|
|
||||||
color: var(--warning);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vm-list {
|
|
||||||
list-style: disc;
|
|
||||||
padding-left: 1.5em;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
</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 {
|
||||||
|
|||||||
@ -108,7 +108,6 @@ export default {
|
|||||||
<YamlEditor
|
<YamlEditor
|
||||||
ref="yamlUser"
|
ref="yamlUser"
|
||||||
v-model:value="config"
|
v-model:value="config"
|
||||||
:mode="mode"
|
|
||||||
class="yaml-editor"
|
class="yaml-editor"
|
||||||
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
|
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
|
||||||
@onChanges="update"
|
@onChanges="update"
|
||||||
|
|||||||
@ -71,38 +71,21 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
let provisioner = `${ this.value.provisioner || LONGHORN_DRIVER }`;
|
||||||
|
|
||||||
|
if (provisioner === LONGHORN_DRIVER) {
|
||||||
|
provisioner = `${ provisioner }_${ this.value.provisionerVersion || LONGHORN_VERSION_V1 }`;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
provisioner,
|
||||||
volumeGroupDialog: null,
|
volumeGroupDialog: null,
|
||||||
randomStr: randomStr(10).toLowerCase(),
|
randomStr: randomStr(10).toLowerCase(),
|
||||||
isOpen: false,
|
isOpen: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
provisioner: {
|
|
||||||
get() {
|
|
||||||
let provisioner = `${ this.value?.provisioner || LONGHORN_DRIVER }`;
|
|
||||||
|
|
||||||
if (provisioner === LONGHORN_DRIVER) {
|
|
||||||
provisioner = `${ provisioner }_${ this.value?.provisionerVersion || LONGHORN_VERSION_V1 }`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return provisioner;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
this.randomStr = randomStr(10).toLowerCase();
|
|
||||||
const [provisioner, provisionerVersion] = (value || '').split('_');
|
|
||||||
|
|
||||||
this.value.provisioner = provisioner;
|
|
||||||
|
|
||||||
if (provisioner === LONGHORN_DRIVER) {
|
|
||||||
this.value.provisionerVersion = provisionerVersion || LONGHORN_VERSION_V1;
|
|
||||||
} else {
|
|
||||||
this.value.provisionerVersion = undefined;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
provisioners() {
|
provisioners() {
|
||||||
const out = [];
|
const out = [];
|
||||||
|
|
||||||
@ -300,6 +283,20 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
provisioner(value) {
|
||||||
|
this.randomStr = randomStr(10).toLowerCase();
|
||||||
|
|
||||||
|
const [provisioner, provisionerVersion] = value?.split('_');
|
||||||
|
|
||||||
|
this.value.provisioner = provisioner;
|
||||||
|
|
||||||
|
if (provisioner === LONGHORN_DRIVER) {
|
||||||
|
this.value.provisionerVersion = provisionerVersion || LONGHORN_VERSION_V1;
|
||||||
|
} else {
|
||||||
|
this.value.provisionerVersion = undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
'value.lvmVolumeGroup'(neu) {
|
'value.lvmVolumeGroup'(neu) {
|
||||||
if (neu === _NEW) {
|
if (neu === _NEW) {
|
||||||
this.value.lvmVolumeGroup = null;
|
this.value.lvmVolumeGroup = null;
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import FileSelector, { createOnSelected } from '@shell/components/form/FileSelec
|
|||||||
|
|
||||||
import { randomStr } from '@shell/utils/string';
|
import { randomStr } from '@shell/utils/string';
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
import { getLoginAwareErrors } from '../utils/error';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditKeypair',
|
name: 'HarvesterEditKeypair',
|
||||||
@ -64,14 +63,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
|
||||||
normalizedErrors() {
|
|
||||||
const message = this.t('harvester.virtualMachine.genericLoginError');
|
|
||||||
|
|
||||||
return getLoginAwareErrors(this.errors, message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: { onKeySelected: createOnSelected('publicKey') },
|
methods: { onKeySelected: createOnSelected('publicKey') },
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -81,9 +72,10 @@ export default {
|
|||||||
:done-route="doneRoute"
|
:done-route="doneRoute"
|
||||||
:resource="value"
|
:resource="value"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:errors="normalizedErrors"
|
:errors="errors"
|
||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
|
@error="e=>errors=e"
|
||||||
>
|
>
|
||||||
<div class="header mb-20">
|
<div class="header mb-20">
|
||||||
<FileSelector
|
<FileSelector
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|||||||
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { NAMESPACE, NODE } from '@shell/config/types';
|
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import { NETWORK_TYPE, L2VLAN_MODE } from '../config/types';
|
import { NETWORK_TYPE, L2VLAN_MODE } from '../config/types';
|
||||||
import { removeObject } from '@shell/utils/array';
|
import { removeObject } from '@shell/utils/array';
|
||||||
@ -21,7 +20,6 @@ const { ACCESS, TRUNK } = L2VLAN_MODE;
|
|||||||
|
|
||||||
const AUTO = 'auto';
|
const AUTO = 'auto';
|
||||||
const MANUAL = 'manual';
|
const MANUAL = 'manual';
|
||||||
const KUBE_SYSTEM = 'kube-system';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ['update:value'],
|
emits: ['update:value'],
|
||||||
@ -72,12 +70,7 @@ export default {
|
|||||||
async fetch() {
|
async fetch() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
await allHash({
|
await allHash({ clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }) });
|
||||||
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
|
|
||||||
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
|
|
||||||
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
|
|
||||||
linkMonitors: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.LINK_MONITOR }),
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
@ -206,80 +199,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.type === UNTAGGED;
|
return this.type === UNTAGGED;
|
||||||
},
|
|
||||||
|
|
||||||
showNicsTab() {
|
|
||||||
return this.isOverlayNetwork && this.value.metadata.namespace === KUBE_SYSTEM;
|
|
||||||
},
|
|
||||||
|
|
||||||
namespaceOptions() {
|
|
||||||
const ns = this.$store.getters['harvester/all'](NAMESPACE) || [];
|
|
||||||
|
|
||||||
// Allow users to select the "kube-system" namespace as the external subnet from Kube-OVN.
|
|
||||||
// This expects the provider network to be in the "kube-system" namespace for VPC NAT gateway functionality.
|
|
||||||
return ns
|
|
||||||
.filter((ns) => !ns.isSystem || ns.id === KUBE_SYSTEM)
|
|
||||||
.map((ns) => ({ name: ns.id }))
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
},
|
|
||||||
|
|
||||||
nodes() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const nodes = this.$store.getters[`${ inStore }/all`](NODE);
|
|
||||||
|
|
||||||
return nodes.filter((n) => n.isEtcd !== 'true');
|
|
||||||
},
|
|
||||||
|
|
||||||
nics() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const linkMonitor = this.$store.getters[`${ inStore }/byId`](HCI.LINK_MONITOR, 'nic') || {};
|
|
||||||
const linkStatus = linkMonitor?.status?.linkStatus || {};
|
|
||||||
const nodes = this.nodes.map((n) => n.id);
|
|
||||||
|
|
||||||
const out = [];
|
|
||||||
|
|
||||||
// Collect all nics from all nodes (for overlay, we select from all nodes)
|
|
||||||
Object.keys(linkStatus).map((nodeName) => {
|
|
||||||
if (nodes.includes(nodeName)) {
|
|
||||||
const nics = linkStatus[nodeName] || [];
|
|
||||||
|
|
||||||
nics.map((nic) => {
|
|
||||||
out.push({
|
|
||||||
...nic,
|
|
||||||
nodeName,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return out;
|
|
||||||
},
|
|
||||||
|
|
||||||
nicOptions() {
|
|
||||||
const out = [];
|
|
||||||
const seen = new Set();
|
|
||||||
|
|
||||||
(this.nics || []).forEach((nic) => {
|
|
||||||
if (!seen.has(nic.name)) {
|
|
||||||
seen.add(nic.name);
|
|
||||||
out.push({
|
|
||||||
label: nic.name,
|
|
||||||
value: nic.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return out.sort((a, b) => a.label.localeCompare(b.label));
|
|
||||||
},
|
|
||||||
|
|
||||||
master: {
|
|
||||||
get() {
|
|
||||||
return this.config?.master || '';
|
|
||||||
},
|
|
||||||
|
|
||||||
set(value) {
|
|
||||||
this.config.master = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -295,7 +214,6 @@ export default {
|
|||||||
this.config.ipam = {};
|
this.config.ipam = {};
|
||||||
this.config.bridge = '';
|
this.config.bridge = '';
|
||||||
delete this.config.provider;
|
delete this.config.provider;
|
||||||
delete this.config.master;
|
|
||||||
delete this.config.server_socket;
|
delete this.config.server_socket;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -312,13 +230,6 @@ export default {
|
|||||||
this.config.vlan = '';
|
this.config.vlan = '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'value.metadata.namespace'(newNamespace) {
|
|
||||||
// NIC selection is only valid for overlay in kube-system namespace.
|
|
||||||
if (newNamespace !== KUBE_SYSTEM) {
|
|
||||||
delete this.config.master;
|
|
||||||
this.value.spec.config = JSON.stringify({ ...this.config });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@ -413,10 +324,6 @@ export default {
|
|||||||
delete this.config.promiscMode;
|
delete this.config.promiscMode;
|
||||||
delete this.config.vlan;
|
delete this.config.vlan;
|
||||||
delete this.config.ipam;
|
delete this.config.ipam;
|
||||||
|
|
||||||
if (this.value.metadata.namespace !== KUBE_SYSTEM) {
|
|
||||||
delete this.config.master;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isUntaggedNetwork) {
|
if (this.isUntaggedNetwork) {
|
||||||
@ -443,7 +350,6 @@ export default {
|
|||||||
ref="nd"
|
ref="nd"
|
||||||
:value="value"
|
:value="value"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:namespace-options="namespaceOptions"
|
|
||||||
@update:value="$emit('update:value', $event)"
|
@update:value="$emit('update:value', $event)"
|
||||||
/>
|
/>
|
||||||
<Tabbed
|
<Tabbed
|
||||||
@ -615,25 +521,6 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
|
||||||
v-if="showNicsTab"
|
|
||||||
name="nics"
|
|
||||||
:label="t('harvester.network.tabs.nic')"
|
|
||||||
:weight="97"
|
|
||||||
class="bordered-table"
|
|
||||||
>
|
|
||||||
<div class="row mt-10">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="master"
|
|
||||||
:label="t('harvester.vlanConfig.uplink.nics.label')"
|
|
||||||
:placeholder="t('harvester.vlanConfig.uplink.nics.overlayWarning')"
|
|
||||||
:mode="mode"
|
|
||||||
:options="nicOptions"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
</Tabbed>
|
</Tabbed>
|
||||||
</CruResource>
|
</CruResource>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -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';
|
||||||
@ -17,7 +16,6 @@ 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 { isBackupTargetSettingEmpty, isBackupTargetSettingUnavailable } from '../utils/setting';
|
||||||
import CronExpressionEditorModal from '@shell/components/Cron/CronExpressionEditorModal.vue';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CreateVMSchedule',
|
name: 'CreateVMSchedule',
|
||||||
@ -30,7 +28,6 @@ export default {
|
|||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
MessageLink,
|
MessageLink,
|
||||||
Banner,
|
Banner,
|
||||||
CronExpressionEditorModal
|
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
@ -89,12 +86,10 @@ 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');
|
||||||
},
|
},
|
||||||
@ -177,9 +172,6 @@ export default {
|
|||||||
this.value.spec['maxFailure'] = this.value.spec.retain;
|
this.value.spec['maxFailure'] = this.value.spec.retain;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openModal() {
|
|
||||||
this.showModel = true;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -264,28 +256,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')"
|
:label="t('harvester.schedule.cron')"
|
||||||
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 +292,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>
|
|
||||||
|
|||||||
@ -3,9 +3,8 @@ import KeyValue from '@shell/components/form/KeyValue';
|
|||||||
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 RadioGroup from '@components/Form/Radio/RadioGroup';
|
import RadioGroup from '@components/Form/Radio/RadioGroup';
|
||||||
import Checkbox from '@components/Form/Checkbox/Checkbox';
|
|
||||||
import { SECRET, LONGHORN } from '@shell/config/types';
|
import { SECRET, LONGHORN } from '@shell/config/types';
|
||||||
import { _CREATE, _VIEW, _EDIT } from '@shell/config/query-params';
|
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
||||||
import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map';
|
import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map';
|
||||||
import { clone } from '@shell/utils/object';
|
import { clone } from '@shell/utils/object';
|
||||||
import { uniq } from '@shell/utils/array';
|
import { uniq } from '@shell/utils/array';
|
||||||
@ -17,9 +16,7 @@ const {
|
|||||||
CSI_NODE_PUBLISH_SECRET_NAME,
|
CSI_NODE_PUBLISH_SECRET_NAME,
|
||||||
CSI_NODE_PUBLISH_SECRET_NAMESPACE,
|
CSI_NODE_PUBLISH_SECRET_NAMESPACE,
|
||||||
CSI_NODE_STAGE_SECRET_NAME,
|
CSI_NODE_STAGE_SECRET_NAME,
|
||||||
CSI_NODE_STAGE_SECRET_NAMESPACE,
|
CSI_NODE_STAGE_SECRET_NAMESPACE
|
||||||
CSI_NODE_EXPAND_SECRET_NAME,
|
|
||||||
CSI_NODE_EXPAND_SECRET_NAMESPACE
|
|
||||||
} = CSI_SECRETS;
|
} = CSI_SECRETS;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -30,7 +27,6 @@ export default {
|
|||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
LabeledInput,
|
LabeledInput,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
Checkbox,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@ -61,10 +57,7 @@ export default {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasExpandSecret = !!(this.value.parameters?.[CSI_NODE_EXPAND_SECRET_NAME] && this.value.parameters?.[CSI_NODE_EXPAND_SECRET_NAMESPACE]);
|
return { };
|
||||||
const volumeExpansionCheckBoxEnabled = this.realMode === _CREATE ? true : hasExpandSecret;
|
|
||||||
|
|
||||||
return { volumeExpansionCheckBoxEnabled };
|
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
@ -105,11 +98,8 @@ export default {
|
|||||||
}, []);
|
}, []);
|
||||||
},
|
},
|
||||||
|
|
||||||
isEdit() {
|
|
||||||
return this.realMode === _EDIT;
|
|
||||||
},
|
|
||||||
isView() {
|
isView() {
|
||||||
return this.realMode === _VIEW;
|
return this.mode === _VIEW;
|
||||||
},
|
},
|
||||||
|
|
||||||
migratableOptions() {
|
migratableOptions() {
|
||||||
@ -162,10 +152,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
enableOnlineExpansionVolumeEncryption() {
|
|
||||||
return this.value.expandOnlineEncryptedVolumeFeatureEnabled;
|
|
||||||
},
|
|
||||||
|
|
||||||
volumeEncryption: {
|
volumeEncryption: {
|
||||||
set(neu) {
|
set(neu) {
|
||||||
this.value['parameters'] = {
|
this.value['parameters'] = {
|
||||||
@ -194,11 +180,6 @@ export default {
|
|||||||
set(selectedSecret) {
|
set(selectedSecret) {
|
||||||
const [namespace, name] = selectedSecret.split('/');
|
const [namespace, name] = selectedSecret.split('/');
|
||||||
|
|
||||||
const expandSecretParams = (this.enableOnlineExpansionVolumeEncryption && this.volumeExpansionCheckBoxEnabled) ? {
|
|
||||||
[CSI_NODE_EXPAND_SECRET_NAME]: name,
|
|
||||||
[CSI_NODE_EXPAND_SECRET_NAMESPACE]: namespace,
|
|
||||||
} : {};
|
|
||||||
|
|
||||||
this.value['parameters'] = {
|
this.value['parameters'] = {
|
||||||
...this.value.parameters,
|
...this.value.parameters,
|
||||||
[CSI_PROVISIONER_SECRET_NAME]: name,
|
[CSI_PROVISIONER_SECRET_NAME]: name,
|
||||||
@ -206,8 +187,7 @@ export default {
|
|||||||
[CSI_NODE_STAGE_SECRET_NAME]: name,
|
[CSI_NODE_STAGE_SECRET_NAME]: name,
|
||||||
[CSI_PROVISIONER_SECRET_NAMESPACE]: namespace,
|
[CSI_PROVISIONER_SECRET_NAMESPACE]: namespace,
|
||||||
[CSI_NODE_PUBLISH_SECRET_NAMESPACE]: namespace,
|
[CSI_NODE_PUBLISH_SECRET_NAMESPACE]: namespace,
|
||||||
[CSI_NODE_STAGE_SECRET_NAMESPACE]: namespace,
|
[CSI_NODE_STAGE_SECRET_NAMESPACE]: namespace
|
||||||
...expandSecretParams,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -260,32 +240,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
|
||||||
volumeExpansionCheckBoxEnabled(enabled) {
|
|
||||||
const currentSecret = this.secret;
|
|
||||||
|
|
||||||
if (!currentSecret) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [namespace, name] = currentSecret.split('/');
|
|
||||||
|
|
||||||
if (enabled && this.enableOnlineExpansionVolumeEncryption) {
|
|
||||||
this.value['parameters'] = {
|
|
||||||
...this.value.parameters,
|
|
||||||
[CSI_NODE_EXPAND_SECRET_NAME]: name,
|
|
||||||
[CSI_NODE_EXPAND_SECRET_NAMESPACE]: namespace,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const params = { ...this.value.parameters };
|
|
||||||
|
|
||||||
delete params[CSI_NODE_EXPAND_SECRET_NAME];
|
|
||||||
delete params[CSI_NODE_EXPAND_SECRET_NAMESPACE];
|
|
||||||
this.value['parameters'] = params;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
@ -383,16 +337,6 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
v-if="enableOnlineExpansionVolumeEncryption"
|
|
||||||
class="col span-6 flex items-center mt-20"
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
v-model:value="volumeExpansionCheckBoxEnabled"
|
|
||||||
:label="t('harvester.storage.volumeExpansionCheckbox')"
|
|
||||||
:disabled="isEdit || isView"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<KeyValue
|
<KeyValue
|
||||||
|
|||||||
@ -254,24 +254,15 @@ export default {
|
|||||||
v-if="restoreNewVm"
|
v-if="restoreNewVm"
|
||||||
v-model:value="restoreResource.spec.keepMacAddress"
|
v-model:value="restoreResource.spec.keepMacAddress"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="check mb-20"
|
|
||||||
:label="t('harvester.backup.restore.keepMacAddress')"
|
:label="t('harvester.backup.restore.keepMacAddress')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-if="!restoreNewVm"
|
v-if="!restoreNewVm"
|
||||||
v-model:value="deletionPolicy"
|
v-model:value="deletionPolicy"
|
||||||
class="mb-20"
|
|
||||||
:label="t('harvester.backup.restore.deletePreviousVolumes')"
|
:label="t('harvester.backup.restore.deletePreviousVolumes')"
|
||||||
:options="deletionPolicyOption"
|
:options="deletionPolicyOption"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Checkbox
|
|
||||||
v-model:value="restoreResource.spec.haltAfterRestore"
|
|
||||||
type="checkbox"
|
|
||||||
class="check mb-20"
|
|
||||||
:label="t('harvester.backup.restore.haltAfterRestore')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer
|
<Footer
|
||||||
|
|||||||
@ -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,10 +22,8 @@ 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';
|
||||||
import Filesystem from './kubevirt.io.virtualmachine/VirtualMachineFilesystem';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditVMTemplate',
|
name: 'HarvesterEditVMTemplate',
|
||||||
@ -41,7 +38,6 @@ export default {
|
|||||||
Network,
|
Network,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
CpuMemory,
|
CpuMemory,
|
||||||
CpuModel,
|
|
||||||
CruResource,
|
CruResource,
|
||||||
CloudConfig,
|
CloudConfig,
|
||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
@ -52,7 +48,6 @@ export default {
|
|||||||
UnitInput,
|
UnitInput,
|
||||||
Banner,
|
Banner,
|
||||||
KeyValue,
|
KeyValue,
|
||||||
Filesystem,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView, VM_MIXIN],
|
mixins: [CreateEditView, VM_MIXIN],
|
||||||
@ -97,10 +92,6 @@ export default {
|
|||||||
secretNamePrefix() {
|
secretNamePrefix() {
|
||||||
return this.templateValue?.metadata?.name;
|
return this.templateValue?.metadata?.name;
|
||||||
},
|
},
|
||||||
|
|
||||||
filesystemEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('supportFilesystem');
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@ -160,22 +151,9 @@ export default {
|
|||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.imageId = this.diskRows[0]?.image || '';
|
this.imageId = this.diskRows[0]?.image || '';
|
||||||
this['filesystemRows'] = this.getFilesystemRows(this.value.spec.vm);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
@ -356,19 +334,6 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
|
||||||
v-if="filesystemEnabled"
|
|
||||||
name="filesystem"
|
|
||||||
:label="t('harvester.tab.filesystem')"
|
|
||||||
:weight="-8"
|
|
||||||
>
|
|
||||||
<Filesystem
|
|
||||||
v-model:value="filesystemRows"
|
|
||||||
:mode="mode"
|
|
||||||
:namespace="templateValue.metadata.namespace"
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
name="labels"
|
name="labels"
|
||||||
:label="t('generic.labels')"
|
:label="t('generic.labels')"
|
||||||
@ -471,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"
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
import Footer from '@shell/components/form/Footer';
|
import Footer from '@shell/components/form/Footer';
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import Checkbox from '@components/Form/Checkbox/Checkbox';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
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';
|
||||||
@ -33,7 +32,6 @@ const createObject = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'CreateRestore',
|
name: 'CreateRestore',
|
||||||
components: {
|
components: {
|
||||||
Checkbox,
|
|
||||||
Footer,
|
Footer,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
LabeledInput,
|
LabeledInput,
|
||||||
@ -251,17 +249,9 @@ export default {
|
|||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-if="!restoreNewVm"
|
v-if="!restoreNewVm"
|
||||||
v-model:value="deletionPolicy"
|
v-model:value="deletionPolicy"
|
||||||
class="mb-20"
|
|
||||||
:label="t('harvester.backup.restore.deletePreviousVolumes')"
|
:label="t('harvester.backup.restore.deletePreviousVolumes')"
|
||||||
:options="deletionPolicyOption"
|
:options="deletionPolicyOption"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Checkbox
|
|
||||||
v-model:value="restoreResource.spec.haltAfterRestore"
|
|
||||||
type="checkbox"
|
|
||||||
class="check mb-20"
|
|
||||||
:label="t('harvester.backup.restore.haltAfterRestore')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer
|
<Footer
|
||||||
|
|||||||
@ -9,11 +9,8 @@ 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 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 +33,6 @@ export default {
|
|||||||
|
|
||||||
components: {
|
components: {
|
||||||
Banner,
|
Banner,
|
||||||
Checkbox,
|
|
||||||
Tab,
|
Tab,
|
||||||
UnitInput,
|
UnitInput,
|
||||||
CruResource,
|
CruResource,
|
||||||
@ -94,8 +90,6 @@ export default {
|
|||||||
source,
|
source,
|
||||||
storage,
|
storage,
|
||||||
imageId,
|
imageId,
|
||||||
showAdvanced: false,
|
|
||||||
createWithDataVolume: false,
|
|
||||||
snapshots: [],
|
snapshots: [],
|
||||||
images: [],
|
images: [],
|
||||||
GIBIBYTE
|
GIBIBYTE
|
||||||
@ -104,24 +98,6 @@ export default {
|
|||||||
|
|
||||||
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 +135,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
|
||||||
@ -303,10 +275,6 @@ export default {
|
|||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
|
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
|
||||||
},
|
},
|
||||||
|
|
||||||
isCreatePVCWithDataVolumeFeatureEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('createPVCWithDataVolume');
|
|
||||||
},
|
|
||||||
|
|
||||||
isResizeDisabled() {
|
isResizeDisabled() {
|
||||||
return (
|
return (
|
||||||
!this.isLHV2VolExpansionFeatureEnabled &&
|
!this.isLHV2VolExpansionFeatureEnabled &&
|
||||||
@ -373,58 +341,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 +383,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 +458,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')"
|
||||||
@ -570,44 +490,6 @@ export default {
|
|||||||
>
|
>
|
||||||
<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"
|
||||||
|
|||||||
@ -1,190 +0,0 @@
|
|||||||
<script>
|
|
||||||
import CruResource from '@shell/components/CruResource';
|
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|
||||||
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
|
||||||
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
|
||||||
import { HCI } from '../types';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
emits: ['update:value'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
CruResource,
|
|
||||||
NameNsDescription,
|
|
||||||
ResourceTabs,
|
|
||||||
Tab,
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
|
||||||
|
|
||||||
inheritAttrs: false,
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
eip: this.value?.spec?.eip || '',
|
|
||||||
externalPort: this.value?.spec?.externalPort || '',
|
|
||||||
internalIp: this.value?.spec?.internalIp || '',
|
|
||||||
internalPort: this.value?.spec?.internalPort || '',
|
|
||||||
protocol: this.value?.spec?.protocol || '',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
await allHash({ eips: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.IPTABLES_EIP }) });
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
if (this.registerBeforeHook) {
|
|
||||||
this.registerBeforeHook(this.updateBeforeSave);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
eipOptions() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const eips = this.$store.getters[`${ inStore }/all`](HCI.IPTABLES_EIP) || [];
|
|
||||||
|
|
||||||
return eips.map((eip) => ({
|
|
||||||
label: eip.id,
|
|
||||||
value: eip.id,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
protocolOptions() {
|
|
||||||
return [
|
|
||||||
{ label: 'TCP', value: 'tcp' },
|
|
||||||
{ label: 'UDP', value: 'udp' },
|
|
||||||
];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
updateBeforeSave() {
|
|
||||||
if (!this.value.spec) {
|
|
||||||
this.value.spec = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value.spec.eip = this.eip;
|
|
||||||
this.value.spec.externalPort = this.externalPort;
|
|
||||||
this.value.spec.internalIp = this.internalIp;
|
|
||||||
this.value.spec.internalPort = this.internalPort;
|
|
||||||
this.value.spec.protocol = this.protocol;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CruResource
|
|
||||||
:done-route="doneRoute"
|
|
||||||
:resource="value"
|
|
||||||
:mode="mode"
|
|
||||||
:errors="errors"
|
|
||||||
:apply-hooks="applyHooks"
|
|
||||||
@finish="save"
|
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
|
||||||
<NameNsDescription
|
|
||||||
ref="nd"
|
|
||||||
:value="value"
|
|
||||||
:mode="mode"
|
|
||||||
:namespaced="false"
|
|
||||||
@update:value="$emit('update:value', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ResourceTabs
|
|
||||||
class="mt-15"
|
|
||||||
:need-conditions="false"
|
|
||||||
:need-related="false"
|
|
||||||
:need-events="false"
|
|
||||||
:side-tabs="true"
|
|
||||||
:mode="mode"
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
name="basic"
|
|
||||||
:label="t('generic.basic')"
|
|
||||||
:weight="99"
|
|
||||||
>
|
|
||||||
<div class="mt-20">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="eip"
|
|
||||||
class="mb-20"
|
|
||||||
:options="eipOptions"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.dnat.eip.label')"
|
|
||||||
:placeholder="t('harvester.dnat.eip.placeholder')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="externalPort"
|
|
||||||
class="mb-20"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.dnat.externalPort.label')"
|
|
||||||
:placeholder="t('harvester.dnat.externalPort.placeholder')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="internalIp"
|
|
||||||
class="mb-20"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.dnat.internalIp.label')"
|
|
||||||
:placeholder="t('harvester.dnat.internalIp.placeholder')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="internalPort"
|
|
||||||
class="mb-20"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.dnat.internalPort.label')"
|
|
||||||
:placeholder="t('harvester.dnat.internalPort.placeholder')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="protocol"
|
|
||||||
class="mb-20"
|
|
||||||
:options="protocolOptions"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.dnat.protocol.label')"
|
|
||||||
:placeholder="t('harvester.dnat.protocol.placeholder')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
</ResourceTabs>
|
|
||||||
</CruResource>
|
|
||||||
</template>
|
|
||||||
@ -1,168 +0,0 @@
|
|||||||
<script>
|
|
||||||
import CruResource from '@shell/components/CruResource';
|
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|
||||||
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
|
||||||
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
|
||||||
import { HCI } from '../types';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
emits: ['update:value'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
CruResource,
|
|
||||||
NameNsDescription,
|
|
||||||
ResourceTabs,
|
|
||||||
Tab,
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
|
||||||
|
|
||||||
inheritAttrs: false,
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
natGwDp: this.value?.spec?.natGwDp || '',
|
|
||||||
externalSubnet: this.value?.spec?.externalSubnet || '',
|
|
||||||
v4ip: this.value?.spec?.v4ip || '',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
await allHash({
|
|
||||||
natGateways: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC_NAT_GATEWAY }),
|
|
||||||
subnets: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SUBNET }),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
if (this.registerBeforeHook) {
|
|
||||||
this.registerBeforeHook(this.updateBeforeSave);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
natGatewayOptions() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const natGateways = this.$store.getters[`${ inStore }/all`](HCI.VPC_NAT_GATEWAY) || [];
|
|
||||||
|
|
||||||
return natGateways.map((gw) => ({
|
|
||||||
label: gw.id,
|
|
||||||
value: gw.id,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
subnetOptions() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const subnets = this.$store.getters[`${ inStore }/all`](HCI.SUBNET) || [];
|
|
||||||
|
|
||||||
return subnets.map((subnet) => ({
|
|
||||||
label: subnet.id,
|
|
||||||
value: subnet.id,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
updateBeforeSave() {
|
|
||||||
if (!this.value.spec) {
|
|
||||||
this.value.spec = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value.spec.natGwDp = this.natGwDp;
|
|
||||||
this.value.spec.externalSubnet = this.externalSubnet;
|
|
||||||
this.value.spec.v4ip = this.v4ip;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CruResource
|
|
||||||
:done-route="doneRoute"
|
|
||||||
:resource="value"
|
|
||||||
:mode="mode"
|
|
||||||
:errors="errors"
|
|
||||||
:apply-hooks="applyHooks"
|
|
||||||
@finish="save"
|
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
|
||||||
<NameNsDescription
|
|
||||||
ref="nd"
|
|
||||||
:value="value"
|
|
||||||
:mode="mode"
|
|
||||||
:namespaced="false"
|
|
||||||
@update:value="$emit('update:value', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ResourceTabs
|
|
||||||
class="mt-15"
|
|
||||||
:need-conditions="false"
|
|
||||||
:need-related="false"
|
|
||||||
:need-events="false"
|
|
||||||
:side-tabs="true"
|
|
||||||
:mode="mode"
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
name="basic"
|
|
||||||
:label="t('generic.basic')"
|
|
||||||
:weight="99"
|
|
||||||
>
|
|
||||||
<div class="mt-20">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="natGwDp"
|
|
||||||
class="mb-20"
|
|
||||||
:options="natGatewayOptions"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.externalIP.natGateway.label')"
|
|
||||||
:placeholder="t('harvester.externalIP.natGateway.placeholder')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="externalSubnet"
|
|
||||||
class="mb-20"
|
|
||||||
:options="subnetOptions"
|
|
||||||
:label="t('harvester.externalIP.externalSubnet.label')"
|
|
||||||
:placeholder="t('harvester.externalIP.externalSubnet.placeholder')"
|
|
||||||
:mode="mode"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="v4ip"
|
|
||||||
class="mb-20"
|
|
||||||
:label="t('harvester.externalIP.v4ip.label')"
|
|
||||||
:placeholder="t('harvester.externalIP.v4ip.placeholder')"
|
|
||||||
:mode="mode"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
</ResourceTabs>
|
|
||||||
</CruResource>
|
|
||||||
</template>
|
|
||||||
@ -1,140 +0,0 @@
|
|||||||
<script>
|
|
||||||
import CruResource from '@shell/components/CruResource';
|
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|
||||||
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
|
||||||
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
|
||||||
import { HCI } from '../types';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
emits: ['update:value'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
CruResource,
|
|
||||||
NameNsDescription,
|
|
||||||
ResourceTabs,
|
|
||||||
Tab,
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
|
||||||
|
|
||||||
inheritAttrs: false,
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
eip: this.value?.spec?.eip || '',
|
|
||||||
internalCIDR: this.value?.spec?.internalCIDR || '',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
await allHash({ eips: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.IPTABLES_EIP }) });
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
if (this.registerBeforeHook) {
|
|
||||||
this.registerBeforeHook(this.updateBeforeSave);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
eipOptions() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const eips = this.$store.getters[`${ inStore }/all`](HCI.IPTABLES_EIP) || [];
|
|
||||||
|
|
||||||
return eips.map((eip) => ({
|
|
||||||
label: eip.id,
|
|
||||||
value: eip.id,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
updateBeforeSave() {
|
|
||||||
if (!this.value.spec) {
|
|
||||||
this.value.spec = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value.spec.eip = this.eip;
|
|
||||||
this.value.spec.internalCIDR = this.internalCIDR;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CruResource
|
|
||||||
:done-route="doneRoute"
|
|
||||||
:resource="value"
|
|
||||||
:mode="mode"
|
|
||||||
:errors="errors"
|
|
||||||
:apply-hooks="applyHooks"
|
|
||||||
@finish="save"
|
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
|
||||||
<NameNsDescription
|
|
||||||
ref="nd"
|
|
||||||
:value="value"
|
|
||||||
:mode="mode"
|
|
||||||
:namespaced="false"
|
|
||||||
@update:value="$emit('update:value', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ResourceTabs
|
|
||||||
class="mt-15"
|
|
||||||
:need-conditions="false"
|
|
||||||
:need-related="false"
|
|
||||||
:need-events="false"
|
|
||||||
:side-tabs="true"
|
|
||||||
:mode="mode"
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
name="basic"
|
|
||||||
:label="t('generic.basic')"
|
|
||||||
:weight="99"
|
|
||||||
>
|
|
||||||
<div class="mt-20">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="eip"
|
|
||||||
class="mb-20"
|
|
||||||
:options="eipOptions"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.snat.eip.label')"
|
|
||||||
:placeholder="t('harvester.snat.eip.placeholder')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="internalCIDR"
|
|
||||||
class="mb-20"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.snat.internalCIDR.label')"
|
|
||||||
:placeholder="t('harvester.snat.internalCIDR.placeholder')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
</ResourceTabs>
|
|
||||||
</CruResource>
|
|
||||||
</template>
|
|
||||||
@ -1,384 +0,0 @@
|
|||||||
<script>
|
|
||||||
import CruResource from '@shell/components/CruResource';
|
|
||||||
import ArrayList from '@shell/components/form/ArrayList';
|
|
||||||
import InfoBox from '@shell/components/InfoBox';
|
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|
||||||
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import ArrayListSelect from '@shell/components/form/ArrayListSelect';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
|
||||||
import { NODE } from '@shell/config/types';
|
|
||||||
import { HCI } from '../types';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
emits: ['update:value'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
CruResource,
|
|
||||||
ArrayList,
|
|
||||||
InfoBox,
|
|
||||||
NameNsDescription,
|
|
||||||
ResourceTabs,
|
|
||||||
Tab,
|
|
||||||
LabeledSelect,
|
|
||||||
ArrayListSelect,
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
|
||||||
|
|
||||||
inheritAttrs: false,
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
defaultInterface: this.value?.spec?.defaultInterface || '',
|
|
||||||
excludedNodes: this.value?.spec?.excludeNodes || [],
|
|
||||||
customInterfaces: this.value?.spec?.customInterfaces || [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
await allHash({
|
|
||||||
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
|
|
||||||
linkMonitors: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.LINK_MONITOR }),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
if (this.registerBeforeHook) {
|
|
||||||
this.registerBeforeHook(this.updateBeforeSave);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
nodes() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const nodes = this.$store.getters[`${ inStore }/all`](NODE);
|
|
||||||
|
|
||||||
return nodes.filter((n) => n.isEtcd !== 'true');
|
|
||||||
},
|
|
||||||
|
|
||||||
nics() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const linkMonitor = this.$store.getters[`${ inStore }/byId`](HCI.LINK_MONITOR, 'nic') || {};
|
|
||||||
const linkStatus = linkMonitor?.status?.linkStatus || {};
|
|
||||||
const nodes = this.nodes.map((n) => n.id);
|
|
||||||
|
|
||||||
const out = [];
|
|
||||||
|
|
||||||
// Collect all nics from all nodes
|
|
||||||
Object.keys(linkStatus).map((nodeName) => {
|
|
||||||
if (nodes.includes(nodeName)) {
|
|
||||||
const nics = linkStatus[nodeName] || [];
|
|
||||||
|
|
||||||
nics.map((nic) => {
|
|
||||||
out.push({
|
|
||||||
...nic,
|
|
||||||
nodeName,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return out;
|
|
||||||
},
|
|
||||||
|
|
||||||
nicOptions() {
|
|
||||||
const out = [];
|
|
||||||
const seen = new Set();
|
|
||||||
|
|
||||||
(this.nics || []).forEach((nic) => {
|
|
||||||
if (!seen.has(nic.name)) {
|
|
||||||
seen.add(nic.name);
|
|
||||||
out.push({
|
|
||||||
label: nic.name,
|
|
||||||
value: nic.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return out.sort((a, b) => a.label.localeCompare(b.label));
|
|
||||||
},
|
|
||||||
|
|
||||||
nodeOptions() {
|
|
||||||
return this.nodes.map((node) => ({
|
|
||||||
label: node.id,
|
|
||||||
value: node.id,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
removeCustomInterface(index) {
|
|
||||||
this.customInterfaces.splice(index, 1);
|
|
||||||
},
|
|
||||||
|
|
||||||
updateBeforeSave() {
|
|
||||||
if (!this.value.spec) {
|
|
||||||
this.value.spec = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value.spec.defaultInterface = this.defaultInterface;
|
|
||||||
this.value.spec.excludeNodes = this.excludedNodes;
|
|
||||||
this.value.spec.customInterfaces = (this.customInterfaces || [])
|
|
||||||
.filter((item) => item?.interface || (item?.nodes || []).length)
|
|
||||||
.map((item) => ({
|
|
||||||
interface: item.interface || '',
|
|
||||||
nodes: (item.nodes || []).filter((node) => !!node),
|
|
||||||
}))
|
|
||||||
.filter((item) => item.interface && item.nodes.length > 0);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CruResource
|
|
||||||
:done-route="doneRoute"
|
|
||||||
:resource="value"
|
|
||||||
:mode="mode"
|
|
||||||
:errors="errors"
|
|
||||||
:apply-hooks="applyHooks"
|
|
||||||
@finish="save"
|
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
|
||||||
<NameNsDescription
|
|
||||||
ref="nd"
|
|
||||||
:value="value"
|
|
||||||
:mode="mode"
|
|
||||||
:namespaced="false"
|
|
||||||
@update:value="$emit('update:value', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ResourceTabs
|
|
||||||
class="mt-15"
|
|
||||||
:need-conditions="false"
|
|
||||||
:need-related="false"
|
|
||||||
:need-events="false"
|
|
||||||
:side-tabs="true"
|
|
||||||
:mode="mode"
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
name="Interfaces"
|
|
||||||
label="Interfaces"
|
|
||||||
:weight="99"
|
|
||||||
>
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="defaultInterface"
|
|
||||||
class="mb-20"
|
|
||||||
required
|
|
||||||
:options="nicOptions"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.providerNetwork.defaultInterface.label')"
|
|
||||||
:placeholder="t('harvester.providerNetwork.defaultInterface.placeholder')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<hr class="section-divider" />
|
|
||||||
|
|
||||||
<ArrayList
|
|
||||||
v-model:value="customInterfaces"
|
|
||||||
class="mb-20 custom-interface-list"
|
|
||||||
:mode="mode"
|
|
||||||
:title="t('harvester.providerNetwork.customInterfaces.label')"
|
|
||||||
:protip="false"
|
|
||||||
:remove-allowed="false"
|
|
||||||
:initial-empty-row="true"
|
|
||||||
:default-add-value="{ interface: '', nodes: [] }"
|
|
||||||
>
|
|
||||||
<template #add="{ add }">
|
|
||||||
<div class="custom-interface-primary-add">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn role-primary"
|
|
||||||
:disabled="mode === 'view'"
|
|
||||||
@click="add"
|
|
||||||
>
|
|
||||||
{{ t('harvester.providerNetwork.customInterfaces.addLabel') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #column-headers>
|
|
||||||
<div class="row custom-interface-header">
|
|
||||||
<div class="col span-6">
|
|
||||||
{{ t('harvester.providerNetwork.customInterfaces.interface.label') }}
|
|
||||||
</div>
|
|
||||||
<div class="col span-6">
|
|
||||||
{{ t('harvester.providerNetwork.customInterfaces.nodes.label') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #columns="scope">
|
|
||||||
<InfoBox class="custom-interface-box">
|
|
||||||
<button
|
|
||||||
v-if="mode !== 'view'"
|
|
||||||
type="button"
|
|
||||||
class="role-link btn btn-sm remove"
|
|
||||||
@click="removeCustomInterface(scope.i)"
|
|
||||||
>
|
|
||||||
<i class="icon icon-x" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="custom-interface-content">
|
|
||||||
<div class="row custom-interface-row interface-row">
|
|
||||||
<div class="col span-12 interface-col">
|
|
||||||
<h3 class="mb-10">
|
|
||||||
{{ t('harvester.providerNetwork.customInterfaces.interface.label') }}
|
|
||||||
</h3>
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="scope.row.value.interface"
|
|
||||||
class="mb-20"
|
|
||||||
:label="''"
|
|
||||||
:options="nicOptions"
|
|
||||||
:mode="mode"
|
|
||||||
:placeholder="t('harvester.providerNetwork.customInterfaces.interface.placeholder')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row custom-interface-row nodes-row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<ArrayListSelect
|
|
||||||
v-model:value="scope.row.value.nodes"
|
|
||||||
:options="nodeOptions"
|
|
||||||
:mode="mode"
|
|
||||||
:disabled="mode === 'view'"
|
|
||||||
:enable-default-add-value="false"
|
|
||||||
:array-list-props="{
|
|
||||||
addLabel: t('harvester.providerNetwork.customInterfaces.nodes.addLabel'),
|
|
||||||
initialEmptyRow: true,
|
|
||||||
title: t('harvester.providerNetwork.customInterfaces.nodes.label'),
|
|
||||||
required: false,
|
|
||||||
protip: false,
|
|
||||||
}"
|
|
||||||
:select-props="{
|
|
||||||
placeholder: t('harvester.providerNetwork.customInterfaces.nodes.placeholder'),
|
|
||||||
disabled: mode === 'view',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<template #add="{ add }">
|
|
||||||
<div class="custom-interface-add">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn role-tertiary add"
|
|
||||||
:disabled="mode === 'view'"
|
|
||||||
@click="add"
|
|
||||||
>
|
|
||||||
{{ t('harvester.providerNetwork.customInterfaces.nodes.addLabel') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ArrayListSelect>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</InfoBox>
|
|
||||||
</template>
|
|
||||||
</ArrayList>
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
|
||||||
name="excludedNodes"
|
|
||||||
:label="t('harvester.providerNetwork.excludedNodes.label')"
|
|
||||||
:weight="98"
|
|
||||||
>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<ArrayListSelect
|
|
||||||
v-model:value="excludedNodes"
|
|
||||||
:options="nodeOptions"
|
|
||||||
:disabled="mode === 'view'"
|
|
||||||
:mode="mode"
|
|
||||||
:enable-default-add-value="false"
|
|
||||||
:array-list-props="{
|
|
||||||
addLabel: t('harvester.providerNetwork.excludedNodes.addLabel'),
|
|
||||||
initialEmptyRow: true,
|
|
||||||
required: false,
|
|
||||||
protip: false,
|
|
||||||
}"
|
|
||||||
:select-props="{
|
|
||||||
placeholder: t('harvester.providerNetwork.excludedNodes.placeholder'),
|
|
||||||
disabled: mode === 'view',
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
</ResourceTabs>
|
|
||||||
</CruResource>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.section-divider {
|
|
||||||
border: none;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
margin: 10px 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-interface-header {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-interface-row {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.interface-row {
|
|
||||||
width: calc(100% - 90px);
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodes-row {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-interface-add {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.nodes-row .array-list-select .box) {
|
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.nodes-row .array-list-select .box .remove) {
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-interface-primary-add {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-interface-box {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.custom-interface-list .box) {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
z-index: 1;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -43,7 +43,6 @@ export default {
|
|||||||
created() {
|
created() {
|
||||||
const vpc = this.$route.query.vpc || '';
|
const vpc = this.$route.query.vpc || '';
|
||||||
const enableDHCP = this.value?.spec?.enableDHCP || false;
|
const enableDHCP = this.value?.spec?.enableDHCP || false;
|
||||||
const natOutgoing = this.value?.spec?.natOutgoing || false;
|
|
||||||
|
|
||||||
set(this.value.spec, 'enableDHCP', enableDHCP);
|
set(this.value.spec, 'enableDHCP', enableDHCP);
|
||||||
set(this.value, 'spec', this.value.spec || {
|
set(this.value, 'spec', this.value.spec || {
|
||||||
@ -51,11 +50,10 @@ export default {
|
|||||||
protocol: NETWORK_PROTOCOL.IPv4,
|
protocol: NETWORK_PROTOCOL.IPv4,
|
||||||
provider: '',
|
provider: '',
|
||||||
vpc,
|
vpc,
|
||||||
gateway: '',
|
gatewayIP: '',
|
||||||
excludeIps: [],
|
excludeIps: [],
|
||||||
private: false,
|
private: false,
|
||||||
enableDHCP,
|
enableDHCP,
|
||||||
natOutgoing,
|
|
||||||
acls: []
|
acls: []
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -66,7 +64,6 @@ export default {
|
|||||||
const hash = {
|
const hash = {
|
||||||
vpc: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
|
vpc: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
|
||||||
nad: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }),
|
nad: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }),
|
||||||
vlans: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN }),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await allHash(hash);
|
await allHash(hash);
|
||||||
@ -132,20 +129,6 @@ export default {
|
|||||||
label: n.id,
|
label: n.id,
|
||||||
value: n.id,
|
value: n.id,
|
||||||
}));
|
}));
|
||||||
},
|
|
||||||
natOutgoingDisabled() {
|
|
||||||
// Disable the NAT Outgoing option when the subnet belongs to the ovn-cluster VPC and its name is join or ovn-default.
|
|
||||||
return this.value?.spec?.vpc === 'ovn-cluster' && ['join', 'ovn-default'].includes(this.value?.metadata?.name);
|
|
||||||
},
|
|
||||||
|
|
||||||
vlanOptions() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const vlans = this.$store.getters[`${ inStore }/all`](HCI.VLAN) || [];
|
|
||||||
|
|
||||||
return vlans.map((vlan) => ({
|
|
||||||
label: vlan.id,
|
|
||||||
value: vlan.id,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -281,16 +264,6 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="value.spec.vlan"
|
|
||||||
class="mb-20"
|
|
||||||
:options="vlanOptions"
|
|
||||||
:placeholder="t('harvester.subnet.vlan.placeholder')"
|
|
||||||
:label="t('harvester.subnet.vlan.label')"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-20">
|
<div class="row mt-20">
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
@ -331,20 +304,6 @@ export default {
|
|||||||
</Banner>
|
</Banner>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<RadioGroup
|
|
||||||
v-model:value="value.spec.natOutgoing"
|
|
||||||
name="enableExternalConnectivity"
|
|
||||||
:disabled="natOutgoingDisabled"
|
|
||||||
:options="[true, false]"
|
|
||||||
:label="t('harvester.subnet.externalConnectivity.label')"
|
|
||||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
|
||||||
:mode="mode"
|
|
||||||
:tooltip="t('harvester.subnet.externalConnectivity.tooltip')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mt-20">
|
<div class="row mt-20">
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
|
|||||||
@ -1,146 +0,0 @@
|
|||||||
<script>
|
|
||||||
import CruResource from '@shell/components/CruResource';
|
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|
||||||
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
|
||||||
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
|
||||||
import { HCI } from '../types';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
emits: ['update:value'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
CruResource,
|
|
||||||
NameNsDescription,
|
|
||||||
ResourceTabs,
|
|
||||||
Tab,
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
|
||||||
|
|
||||||
inheritAttrs: false,
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
vlanId: this.value?.spec?.id || '',
|
|
||||||
provider: this.value?.spec?.provider || '',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
await allHash({ providerNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.PROVIDER_NETWORK }) });
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
if (this.registerBeforeHook) {
|
|
||||||
this.registerBeforeHook(this.updateBeforeSave);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
providerNetworks() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const providerNetworks = this.$store.getters[`${ inStore }/all`](HCI.PROVIDER_NETWORK) || [];
|
|
||||||
|
|
||||||
return providerNetworks.map((pn) => ({
|
|
||||||
label: pn.id,
|
|
||||||
value: pn.id,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
updateBeforeSave() {
|
|
||||||
if (!this.value.spec) {
|
|
||||||
this.value.spec = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.vlanId !== '') {
|
|
||||||
this.value.spec.id = Number(this.vlanId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value.spec.provider = this.provider;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CruResource
|
|
||||||
:done-route="doneRoute"
|
|
||||||
:resource="value"
|
|
||||||
:mode="mode"
|
|
||||||
:errors="errors"
|
|
||||||
:apply-hooks="applyHooks"
|
|
||||||
@finish="save"
|
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
|
||||||
<NameNsDescription
|
|
||||||
ref="nd"
|
|
||||||
:value="value"
|
|
||||||
:mode="mode"
|
|
||||||
:namespaced="false"
|
|
||||||
@update:value="$emit('update:value', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ResourceTabs
|
|
||||||
class="mt-15"
|
|
||||||
:need-conditions="false"
|
|
||||||
:need-related="false"
|
|
||||||
:need-events="false"
|
|
||||||
:side-tabs="true"
|
|
||||||
:mode="mode"
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
name="basic"
|
|
||||||
:label="t('generic.basic')"
|
|
||||||
:weight="99"
|
|
||||||
>
|
|
||||||
<div class="mt-20">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value.number="vlanId"
|
|
||||||
class="mb-20"
|
|
||||||
type="number"
|
|
||||||
:min="1"
|
|
||||||
:max="4094"
|
|
||||||
:label="t('harvester.vlan.id.label')"
|
|
||||||
:placeholder="t('harvester.vlan.id.placeholder')"
|
|
||||||
:mode="mode"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="provider"
|
|
||||||
class="mb-20"
|
|
||||||
:options="providerNetworks"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.vlan.provider.label')"
|
|
||||||
:placeholder="t('harvester.vlan.provider.placeholder')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
</ResourceTabs>
|
|
||||||
</CruResource>
|
|
||||||
</template>
|
|
||||||
@ -1,246 +0,0 @@
|
|||||||
<script>
|
|
||||||
import CruResource from '@shell/components/CruResource';
|
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|
||||||
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
|
||||||
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import ArrayListSelect from '@shell/components/form/ArrayListSelect';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/config/labels-annotations';
|
|
||||||
import { HCI } from '../types';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
emits: ['update:value'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
CruResource,
|
|
||||||
NameNsDescription,
|
|
||||||
ResourceTabs,
|
|
||||||
Tab,
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
ArrayListSelect,
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
|
||||||
|
|
||||||
inheritAttrs: false,
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
const internalTenantNetwork = this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.CNI_NETWORKS] || '';
|
|
||||||
|
|
||||||
return {
|
|
||||||
internalTenantNetwork,
|
|
||||||
vpc: this.value?.spec?.vpc || '',
|
|
||||||
subnet: this.value?.spec?.subnet || '',
|
|
||||||
lanIp: this.value?.spec?.lanIp || '',
|
|
||||||
externalSubnets: this.value?.spec?.externalSubnets || [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
await allHash({
|
|
||||||
vpcs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
|
|
||||||
subnets: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SUBNET }),
|
|
||||||
vmNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.NETWORK_ATTACHMENT }),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
if (this.registerBeforeHook) {
|
|
||||||
this.registerBeforeHook(this.updateBeforeSave);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
vpcOptions() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const vpcs = this.$store.getters[`${ inStore }/all`](HCI.VPC) || [];
|
|
||||||
|
|
||||||
return vpcs.map((vpc) => ({
|
|
||||||
label: vpc.id,
|
|
||||||
value: vpc.id,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
subnetOptions() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const subnets = this.$store.getters[`${ inStore }/all`](HCI.SUBNET) || [];
|
|
||||||
|
|
||||||
return subnets.map((subnet) => ({
|
|
||||||
label: subnet.id,
|
|
||||||
value: subnet.id,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
vmNetworkOptions() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const vmNetworks = this.$store.getters[`${ inStore }/all`](HCI.NETWORK_ATTACHMENT) || [];
|
|
||||||
|
|
||||||
return vmNetworks.map((network) => ({
|
|
||||||
label: network.id,
|
|
||||||
value: network.id,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
updateBeforeSave() {
|
|
||||||
if (!this.value.spec) {
|
|
||||||
this.value.spec = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.value.metadata) {
|
|
||||||
this.value.metadata = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.value.metadata.annotations) {
|
|
||||||
this.value.metadata.annotations = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value.spec.vpc = this.vpc;
|
|
||||||
this.value.spec.subnet = this.subnet;
|
|
||||||
this.value.spec.lanIp = this.lanIp;
|
|
||||||
this.value.spec.externalSubnets = (this.externalSubnets || []).filter((subnet) => !!subnet);
|
|
||||||
|
|
||||||
if (this.internalTenantNetwork) {
|
|
||||||
this.value.metadata.annotations[HCI_ANNOTATIONS.CNI_NETWORKS] = this.internalTenantNetwork;
|
|
||||||
} else {
|
|
||||||
delete this.value.metadata.annotations[HCI_ANNOTATIONS.CNI_NETWORKS];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CruResource
|
|
||||||
:done-route="doneRoute"
|
|
||||||
:resource="value"
|
|
||||||
:mode="mode"
|
|
||||||
:errors="errors"
|
|
||||||
:apply-hooks="applyHooks"
|
|
||||||
@finish="save"
|
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
|
||||||
<NameNsDescription
|
|
||||||
ref="nd"
|
|
||||||
:value="value"
|
|
||||||
:mode="mode"
|
|
||||||
:namespaced="false"
|
|
||||||
@update:value="$emit('update:value', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ResourceTabs
|
|
||||||
class="mt-15"
|
|
||||||
:need-conditions="false"
|
|
||||||
:need-related="false"
|
|
||||||
:need-events="false"
|
|
||||||
:side-tabs="true"
|
|
||||||
:mode="mode"
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
name="basic"
|
|
||||||
:label="t('generic.basic')"
|
|
||||||
:weight="99"
|
|
||||||
>
|
|
||||||
<div class="mt-20">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="internalTenantNetwork"
|
|
||||||
class="mb-20"
|
|
||||||
required
|
|
||||||
:options="vmNetworkOptions"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.natGateway.internalTenantNetwork.label')"
|
|
||||||
:placeholder="t('harvester.natGateway.internalTenantNetwork.placeholder')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="vpc"
|
|
||||||
class="mb-20"
|
|
||||||
:options="vpcOptions"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.natGateway.vpc.label')"
|
|
||||||
:placeholder="t('harvester.natGateway.vpc.placeholder')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="subnet"
|
|
||||||
class="mb-20"
|
|
||||||
:options="subnetOptions"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.natGateway.subnet.label')"
|
|
||||||
:placeholder="t('harvester.natGateway.subnet.placeholder')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="lanIp"
|
|
||||||
class="mb-20"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.natGateway.lanIp.label')"
|
|
||||||
:placeholder="t('harvester.natGateway.lanIp.placeholder')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
|
||||||
name="externalSubnets"
|
|
||||||
:label="t('harvester.natGateway.externalSubnets.label')"
|
|
||||||
:weight="98"
|
|
||||||
>
|
|
||||||
<div class="mt-20">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<ArrayListSelect
|
|
||||||
v-model:value="externalSubnets"
|
|
||||||
:mode="mode"
|
|
||||||
:disabled="mode === 'view'"
|
|
||||||
required
|
|
||||||
:options="subnetOptions"
|
|
||||||
:enable-default-add-value="false"
|
|
||||||
:array-list-props="{
|
|
||||||
addLabel: t('harvester.natGateway.externalSubnets.addLabel'),
|
|
||||||
title: t('harvester.natGateway.subnet.label'),
|
|
||||||
initialEmptyRow: true,
|
|
||||||
required: true,
|
|
||||||
protip: false,
|
|
||||||
}"
|
|
||||||
:select-props="{
|
|
||||||
placeholder: t('harvester.natGateway.externalSubnets.placeholder'),
|
|
||||||
disabled: mode === 'view',
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
</ResourceTabs>
|
|
||||||
</CruResource>
|
|
||||||
</template>
|
|
||||||
@ -143,7 +143,6 @@ export default {
|
|||||||
<YamlEditor
|
<YamlEditor
|
||||||
ref="yaml"
|
ref="yaml"
|
||||||
v-model:value="yamlScript"
|
v-model:value="yamlScript"
|
||||||
:mode="mode"
|
|
||||||
class="yaml-editor"
|
class="yaml-editor"
|
||||||
:editor-mode="editorMode"
|
:editor-mode="editorMode"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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,310 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import { CONFIG_MAP, SECRET, SERVICE_ACCOUNT } from '@shell/config/types';
|
|
||||||
import { _VIEW } from '@shell/config/query-params';
|
|
||||||
import CopyToClipboard from '@shell/components/CopyToClipboard';
|
|
||||||
import MessageLink from '@shell/components/MessageLink';
|
|
||||||
import { FILESYSTEM_SOURCE_TYPE } from '@pkg/harvester/config/types';
|
|
||||||
|
|
||||||
const MAX_FILESYSTEMS = 3;
|
|
||||||
|
|
||||||
const { CONFIGMAP: FS_TYPE_CONFIGMAP, SECRET: FS_TYPE_SECRET, SERVICEACCOUNT: FS_TYPE_SERVICEACCOUNT } = FILESYSTEM_SOURCE_TYPE;
|
|
||||||
|
|
||||||
const DEFAULT_VOLUME_NAMES = {
|
|
||||||
[FS_TYPE_CONFIGMAP]: 'appconfigfs',
|
|
||||||
[FS_TYPE_SECRET]: 'appsecretfs',
|
|
||||||
[FS_TYPE_SERVICEACCOUNT]: 'appserviceaccountfs',
|
|
||||||
};
|
|
||||||
|
|
||||||
const FS_TYPE_OPTIONS = [
|
|
||||||
{ label: 'ConfigMap', value: FS_TYPE_CONFIGMAP },
|
|
||||||
{ label: 'Secret', value: FS_TYPE_SECRET },
|
|
||||||
{ label: 'ServiceAccount', value: FS_TYPE_SERVICEACCOUNT },
|
|
||||||
];
|
|
||||||
|
|
||||||
function emptyRow() {
|
|
||||||
return {
|
|
||||||
fsType: FS_TYPE_CONFIGMAP,
|
|
||||||
volumeName: DEFAULT_VOLUME_NAMES[FS_TYPE_CONFIGMAP],
|
|
||||||
resourceName: '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'VirtualMachineFilesystem',
|
|
||||||
|
|
||||||
emits: ['update:value'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
Banner,
|
|
||||||
LabeledSelect,
|
|
||||||
LabeledInput,
|
|
||||||
CopyToClipboard,
|
|
||||||
MessageLink,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
default: 'create',
|
|
||||||
},
|
|
||||||
namespace: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return { rows: this.value.length > 0 ? this.value.map((r) => ({ ...r })) : [emptyRow()] };
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
value(newVal) {
|
|
||||||
if (newVal) {
|
|
||||||
const incoming = JSON.stringify(newVal);
|
|
||||||
const current = JSON.stringify(this.rows);
|
|
||||||
|
|
||||||
if (incoming !== current) {
|
|
||||||
this.rows = newVal.map((r) => ({ ...r }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
rows: {
|
|
||||||
deep: true,
|
|
||||||
handler(val) {
|
|
||||||
this.$emit('update:value', val.map((r) => ({ ...r })));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
inStore() {
|
|
||||||
return this.$store.getters['currentProduct'].inStore;
|
|
||||||
},
|
|
||||||
|
|
||||||
configMaps() {
|
|
||||||
return this.$store.getters[`${ this.inStore }/all`](CONFIG_MAP)
|
|
||||||
.filter((cm) => !this.namespace || cm.metadata.namespace === this.namespace)
|
|
||||||
.map((cm) => ({ label: cm.metadata.name, value: cm.metadata.name }));
|
|
||||||
},
|
|
||||||
|
|
||||||
secrets() {
|
|
||||||
return this.$store.getters[`${ this.inStore }/all`](SECRET)
|
|
||||||
.filter((s) => !this.namespace || s.metadata.namespace === this.namespace)
|
|
||||||
.map((s) => ({ label: s.metadata.name, value: s.metadata.name }));
|
|
||||||
},
|
|
||||||
|
|
||||||
serviceAccounts() {
|
|
||||||
return this.$store.getters[`${ this.inStore }/all`](SERVICE_ACCOUNT)
|
|
||||||
.filter((sa) => !this.namespace || sa.metadata.namespace === this.namespace)
|
|
||||||
.map((sa) => ({ label: sa.metadata.name, value: sa.metadata.name }));
|
|
||||||
},
|
|
||||||
|
|
||||||
canAddRow() {
|
|
||||||
return this.rows.length < MAX_FILESYSTEMS;
|
|
||||||
},
|
|
||||||
|
|
||||||
isView() {
|
|
||||||
return this.mode === _VIEW;
|
|
||||||
},
|
|
||||||
|
|
||||||
completedRows() {
|
|
||||||
return this.rows.filter((r) => r.fsType && r.volumeName && r.resourceName);
|
|
||||||
},
|
|
||||||
|
|
||||||
allMountCommands() {
|
|
||||||
return this.completedRows.map((r) => this.mountCommands(r)).join('\n');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
fsTypeOptions(currentIndex) {
|
|
||||||
const usedTypes = this.rows
|
|
||||||
.filter((_, i) => i !== currentIndex)
|
|
||||||
.map((r) => r.fsType);
|
|
||||||
|
|
||||||
return FS_TYPE_OPTIONS.filter((opt) => !usedTypes.includes(opt.value));
|
|
||||||
},
|
|
||||||
|
|
||||||
resourceOptions(fsType) {
|
|
||||||
if (fsType === FS_TYPE_CONFIGMAP) return this.configMaps;
|
|
||||||
if (fsType === FS_TYPE_SECRET) return this.secrets;
|
|
||||||
if (fsType === FS_TYPE_SERVICEACCOUNT) return this.serviceAccounts;
|
|
||||||
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
|
|
||||||
onFsTypeChange(row, newType) {
|
|
||||||
row.fsType = newType;
|
|
||||||
row.volumeName = DEFAULT_VOLUME_NAMES[newType] || '';
|
|
||||||
row.resourceName = '';
|
|
||||||
},
|
|
||||||
|
|
||||||
addRow() {
|
|
||||||
if (this.canAddRow) {
|
|
||||||
const usedTypes = this.rows.map((r) => r.fsType);
|
|
||||||
const nextType = FS_TYPE_OPTIONS.find((opt) => !usedTypes.includes(opt.value))?.value || FS_TYPE_CONFIGMAP;
|
|
||||||
|
|
||||||
this.rows.push({
|
|
||||||
fsType: nextType,
|
|
||||||
volumeName: DEFAULT_VOLUME_NAMES[nextType] || '',
|
|
||||||
resourceName: '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
removeRow(index) {
|
|
||||||
this.rows.splice(index, 1);
|
|
||||||
},
|
|
||||||
|
|
||||||
mountCommands(row) {
|
|
||||||
const vol = row.volumeName || '<volume-name>';
|
|
||||||
|
|
||||||
return `- mkdir -p /mnt/${ vol }\n- mount -t virtiofs ${ vol } /mnt/${ vol }`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="vm-filesystem">
|
|
||||||
<p class="mb-20">
|
|
||||||
{{ t('harvester.virtualMachine.filesystem.description') }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-for="(row, index) in rows"
|
|
||||||
:key="index"
|
|
||||||
class="filesystem-row mb-15"
|
|
||||||
>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-3">
|
|
||||||
<LabeledSelect
|
|
||||||
:value="row.fsType"
|
|
||||||
:label="t('harvester.virtualMachine.filesystem.type')"
|
|
||||||
:options="fsTypeOptions(index)"
|
|
||||||
:mode="mode"
|
|
||||||
required
|
|
||||||
@update:value="onFsTypeChange(row, $event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col span-3">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="row.volumeName"
|
|
||||||
:label="t('harvester.virtualMachine.filesystem.volume')"
|
|
||||||
:mode="mode"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col span-5">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="row.resourceName"
|
|
||||||
:label="t('harvester.virtualMachine.filesystem.resource')"
|
|
||||||
:options="resourceOptions(row.fsType)"
|
|
||||||
:mode="mode"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="!isView"
|
|
||||||
class="col span-1 remove-col"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn role-link remove-btn"
|
|
||||||
@click="removeRow(index)"
|
|
||||||
>
|
|
||||||
{{ t('generic.remove') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Banner
|
|
||||||
v-if="completedRows.length > 0 && mode === 'create'"
|
|
||||||
color="warning"
|
|
||||||
class="mt-10"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<MessageLink
|
|
||||||
:to="{ hash: '#advanced' }"
|
|
||||||
prefix-label="harvester.virtualMachine.filesystem.mountBannerHint"
|
|
||||||
middle-label="harvester.virtualMachine.filesystem.mountBannerHintLink"
|
|
||||||
suffix-label="harvester.virtualMachine.filesystem.mountBannerHintSuffix"
|
|
||||||
/>
|
|
||||||
<div class="pre-wrapper mt-10">
|
|
||||||
<pre class="mt-5 mb-0">{{ allMountCommands }}</pre>
|
|
||||||
<CopyToClipboard
|
|
||||||
:text="allMountCommands"
|
|
||||||
:show-label="false"
|
|
||||||
class="icon-btn"
|
|
||||||
action-color="bg-transparent"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Banner>
|
|
||||||
|
|
||||||
<button
|
|
||||||
v-if="!isView && canAddRow"
|
|
||||||
type="button"
|
|
||||||
class="btn role-tertiary add"
|
|
||||||
@click="addRow"
|
|
||||||
>
|
|
||||||
{{ t('harvester.virtualMachine.filesystem.add') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.vm-filesystem {
|
|
||||||
padding: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filesystem-row {
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
padding-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-col {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-btn {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pre-wrapper {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
pre {
|
|
||||||
padding-right: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn {
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
right: 5px;
|
|
||||||
padding: 2px 4px;
|
|
||||||
line-height: 1;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: var(--success) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -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];
|
||||||
@ -152,10 +138,6 @@ export default {
|
|||||||
return !rows.find((device) => !device.passthroughClaim);
|
return !rows.find((device) => !device.passthroughClaim);
|
||||||
},
|
},
|
||||||
|
|
||||||
canManageGroup(rows = []) {
|
|
||||||
return rows.length > 0 && rows.every((row) => row.canUpdate === true);
|
|
||||||
},
|
|
||||||
|
|
||||||
changeRows(filterRows, parentSriov) {
|
changeRows(filterRows, parentSriov) {
|
||||||
this['filterRows'] = filterRows;
|
this['filterRows'] = filterRows;
|
||||||
this['parentSriov'] = parentSriov;
|
this['parentSriov'] = parentSriov;
|
||||||
@ -188,10 +170,6 @@ export default {
|
|||||||
:ref="group.key"
|
:ref="group.key"
|
||||||
v-trim-whitespace
|
v-trim-whitespace
|
||||||
class="group-tab"
|
class="group-tab"
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="canManageGroup(group.rows)"
|
|
||||||
class="group-actions"
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="groupIsAllEnabled(group.rows)"
|
v-if="groupIsAllEnabled(group.rows)"
|
||||||
@ -209,7 +187,6 @@ export default {
|
|||||||
>
|
>
|
||||||
{{ t('harvester.pci.enableGroup') }}
|
{{ t('harvester.pci.enableGroup') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
<span v-clean-html="group.key" />
|
<span v-clean-html="group.key" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -229,21 +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>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.group-actions {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -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: {
|
||||||
@ -52,21 +50,26 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectedDevices = [];
|
const selectedDevices = [];
|
||||||
|
const oldFormatDevices = [];
|
||||||
|
|
||||||
const vmDevices = this.value?.domain?.devices?.hostDevices || [];
|
const vmDevices = this.value?.domain?.devices?.hostDevices || [];
|
||||||
const vmDeviceNames = vmDevices.map(({ name }) => name);
|
const otherDevices = this.otherDevices(vmDevices).map(({ name }) => name);
|
||||||
|
|
||||||
this.pciDevices.forEach((row) => {
|
vmDevices.forEach(({ name, deviceName }) => {
|
||||||
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
|
const checkName = (deviceName || '').split('/')?.[1];
|
||||||
});
|
|
||||||
|
|
||||||
vmDevices.forEach(({ name }) => {
|
if (checkName && name.includes(checkName) && !otherDevices.includes(name)) {
|
||||||
if (this.enabledDevices.find((device) => device?.metadata?.name === name)) {
|
oldFormatDevices.push(name);
|
||||||
|
} else if (this.enabledDevices.find((device) => device?.metadata?.name === name)) {
|
||||||
selectedDevices.push(name);
|
selectedDevices.push(name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (oldFormatDevices.length > 0) {
|
||||||
|
this.oldFormatDevices = oldFormatDevices;
|
||||||
|
} else {
|
||||||
this.selectedDevices = selectedDevices;
|
this.selectedDevices = selectedDevices;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@ -77,6 +80,7 @@ export default {
|
|||||||
selectedDevices: [],
|
selectedDevices: [],
|
||||||
pciDeviceSchema: this.$store.getters['harvester/schemaFor'](HCI.PCI_DEVICE),
|
pciDeviceSchema: this.$store.getters['harvester/schemaFor'](HCI.PCI_DEVICE),
|
||||||
showMatrix: false,
|
showMatrix: false,
|
||||||
|
oldFormatDevices: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -129,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;
|
||||||
@ -188,6 +185,11 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
oldFormatDevicesHTML() {
|
||||||
|
return this.oldFormatDevices.map((device) => {
|
||||||
|
return `<li>${ device }</li>`;
|
||||||
|
}).join('');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@ -211,15 +213,21 @@ export default {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="oldFormatDevices.length > 0"
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div class="col span-12">
|
||||||
|
<Banner color="warning">
|
||||||
|
<p v-clean-html="t('harvester.pci.oldFormatDevices.help', {oldFormatDevicesHTML}, true)" />
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
<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"
|
||||||
@ -296,4 +304,5 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import { _VIEW } from '@shell/config/query-params';
|
|||||||
|
|
||||||
import { NAMESPACE } from '@shell/config/types';
|
import { NAMESPACE } from '@shell/config/types';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
import { getLoginAwareErrors } from '../../utils/error';
|
|
||||||
|
|
||||||
const _NEW = '_NEW';
|
const _NEW = '_NEW';
|
||||||
|
|
||||||
@ -215,9 +214,7 @@ export default {
|
|||||||
buttonCb(true);
|
buttonCb(true);
|
||||||
this.cancel();
|
this.cancel();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = this.t('harvester.virtualMachine.genericLoginError');
|
this.errors = [err.message];
|
||||||
|
|
||||||
this.errors = getLoginAwareErrors(err, message);
|
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -64,12 +64,6 @@ export default {
|
|||||||
value: 'status.productID',
|
value: 'status.productID',
|
||||||
sort: ['status.productID', 'status.vendorID']
|
sort: ['status.productID', 'status.vendorID']
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'classType',
|
|
||||||
labelKey: 'harvester.usb.classType',
|
|
||||||
value: 'status.classType',
|
|
||||||
sort: ['status.classType']
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!isSingleProduct) {
|
if (!isSingleProduct) {
|
||||||
@ -113,11 +107,6 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
canManageGroup(rows = []) {
|
|
||||||
return rows.every((row) => row.canUpdate === true);
|
|
||||||
},
|
|
||||||
|
|
||||||
groupIsAllEnabled(rows = []) {
|
groupIsAllEnabled(rows = []) {
|
||||||
return !rows.find((device) => !device.passthroughClaim);
|
return !rows.find((device) => !device.passthroughClaim);
|
||||||
},
|
},
|
||||||
@ -157,10 +146,6 @@ export default {
|
|||||||
:ref="group.key"
|
:ref="group.key"
|
||||||
v-trim-whitespace
|
v-trim-whitespace
|
||||||
class="group-tab"
|
class="group-tab"
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="canManageGroup(group.rows)"
|
|
||||||
class="group-actions"
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="groupIsAllEnabled(group.rows)"
|
v-if="groupIsAllEnabled(group.rows)"
|
||||||
@ -178,7 +163,6 @@ export default {
|
|||||||
>
|
>
|
||||||
{{ t('harvester.usb.enableGroup') }}
|
{{ t('harvester.usb.enableGroup') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
<span v-clean-html="group.key" />
|
<span v-clean-html="group.key" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -191,9 +175,3 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
</ResourceTable>
|
</ResourceTable>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.group-actions {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
@ -151,7 +146,7 @@ export default {
|
|||||||
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 +217,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) {
|
||||||
@ -391,12 +346,12 @@ export default {
|
|||||||
<i class="icon icon-x" />
|
<i class="icon icon-x" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canDoVolumeHotplugAction(volume) && isView"
|
v-if="volume.hotpluggable && isView"
|
||||||
type="button"
|
type="button"
|
||||||
class="role-link btn btn-sm remove"
|
class="role-link btn btn-sm remove"
|
||||||
@click="hotplugVolume(volume)"
|
@click="unplugVolume(volume)"
|
||||||
>
|
>
|
||||||
{{ t(getVolumeHotplugActionLabel(volume)) }}
|
{{ t('harvester.virtualMachine.hotUnplug.detachVolume.actionLabel') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -475,6 +430,7 @@ export default {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm bg-primary mr-15 mb-10"
|
class="btn btn-sm bg-primary mr-15 mb-10"
|
||||||
|
:disabled="rows.length === 0"
|
||||||
@click="addVolume(SOURCE_TYPE.NEW)"
|
@click="addVolume(SOURCE_TYPE.NEW)"
|
||||||
>
|
>
|
||||||
{{ t('harvester.virtualMachine.volume.addVolume') }}
|
{{ t('harvester.virtualMachine.volume.addVolume') }}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import Tabbed from '@shell/components/Tabbed';
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
import { clone, set } from '@shell/utils/object';
|
import { clone } from '@shell/utils/object';
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
import CruResource from '@shell/components/CruResource';
|
import CruResource from '@shell/components/CruResource';
|
||||||
@ -21,7 +21,9 @@ import { saferDump } from '@shell/utils/create-yaml';
|
|||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
import { BEFORE_SAVE_HOOKS, AFTER_SAVE_HOOKS } from '@shell/mixins/child-hook';
|
import { BEFORE_SAVE_HOOKS, AFTER_SAVE_HOOKS } from '@shell/mixins/child-hook';
|
||||||
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
|
||||||
import { parseVolumeClaimTemplates } from '@pkg/utils/vm';
|
import { parseVolumeClaimTemplates } from '@pkg/utils/vm';
|
||||||
import VM_MIXIN from '../../mixins/harvester-vm';
|
import VM_MIXIN from '../../mixins/harvester-vm';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
@ -30,12 +32,10 @@ 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';
|
||||||
import Reserved from './VirtualMachineReserved';
|
import Reserved from './VirtualMachineReserved';
|
||||||
import Filesystem from './VirtualMachineFilesystem';
|
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import MessageLink from '@shell/components/MessageLink';
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
|
|
||||||
@ -57,7 +57,6 @@ export default {
|
|||||||
SSHKey,
|
SSHKey,
|
||||||
Network,
|
Network,
|
||||||
CpuMemory,
|
CpuMemory,
|
||||||
CpuModel,
|
|
||||||
CloudConfig,
|
CloudConfig,
|
||||||
NodeScheduling,
|
NodeScheduling,
|
||||||
PodAffinity,
|
PodAffinity,
|
||||||
@ -71,7 +70,6 @@ export default {
|
|||||||
Banner,
|
Banner,
|
||||||
MessageLink,
|
MessageLink,
|
||||||
UsbDevices,
|
UsbDevices,
|
||||||
Filesystem,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView, VM_MIXIN],
|
mixins: [CreateEditView, VM_MIXIN],
|
||||||
@ -91,8 +89,6 @@ export default {
|
|||||||
|
|
||||||
const hostname = this.value.spec.template.spec.hostname || '';
|
const hostname = this.value.spec.template.spec.hostname || '';
|
||||||
|
|
||||||
const customizeDisplayName = !!(this.value.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cloneVM,
|
cloneVM,
|
||||||
count: 2,
|
count: 2,
|
||||||
@ -103,24 +99,12 @@ export default {
|
|||||||
isOpen: false,
|
isOpen: false,
|
||||||
hostname,
|
hostname,
|
||||||
isRestartImmediately,
|
isRestartImmediately,
|
||||||
customizeDisplayName,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
// VM display name is stored as an annotation; bind a dedicated input to it
|
|
||||||
// so we don't have to mutate metadata.name (which would break the k8s PUT).
|
|
||||||
displayName: {
|
|
||||||
get() {
|
|
||||||
return this.value.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME] || '';
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.value.setAnnotation(HCI_ANNOTATIONS.VM_DISPLAY_NAME, val);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
to() {
|
to() {
|
||||||
return {
|
return {
|
||||||
name: 'harvester-c-cluster-resource',
|
name: 'harvester-c-cluster-resource',
|
||||||
@ -225,16 +209,9 @@ 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');
|
||||||
},
|
},
|
||||||
filesystemEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('supportFilesystem');
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@ -306,12 +283,6 @@ export default {
|
|||||||
this.getInitConfig({ value: this.value, init: this.isCreate });
|
this.getInitConfig({ value: this.value, init: this.isCreate });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
customizeDisplayName(neu) {
|
|
||||||
if (!neu) {
|
|
||||||
this.value.setAnnotation(HCI_ANNOTATIONS.VM_DISPLAY_NAME, '');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
@ -344,7 +315,6 @@ export default {
|
|||||||
const diskRows = this.getDiskRows(this.value);
|
const diskRows = this.getDiskRows(this.value);
|
||||||
|
|
||||||
this['diskRows'] = diskRows;
|
this['diskRows'] = diskRows;
|
||||||
this['filesystemRows'] = this.getFilesystemRows(this.value);
|
|
||||||
const templateId = this.$route.query.templateId;
|
const templateId = this.$route.query.templateId;
|
||||||
const templateVersionId = this.$route.query.versionId;
|
const templateVersionId = this.$route.query.versionId;
|
||||||
|
|
||||||
@ -568,18 +538,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>
|
||||||
@ -634,33 +592,6 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
</NameNsDescription>
|
</NameNsDescription>
|
||||||
|
|
||||||
<div v-if="isSingle">
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-12">
|
|
||||||
<Checkbox
|
|
||||||
v-model:value="customizeDisplayName"
|
|
||||||
class="check"
|
|
||||||
type="checkbox"
|
|
||||||
:label="t('harvester.virtualMachine.input.customizeDisplayName')"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="customizeDisplayName"
|
|
||||||
class="row mb-20"
|
|
||||||
>
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="displayName"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.virtualMachine.input.displayName')"
|
|
||||||
:placeholder="t('harvester.virtualMachine.input.displayNamePlaceholder')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-if="isCreate"
|
v-if="isCreate"
|
||||||
v-model:value="useTemplate"
|
v-model:value="useTemplate"
|
||||||
@ -795,7 +726,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"
|
||||||
@ -834,23 +765,10 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
|
||||||
v-if="filesystemEnabled"
|
|
||||||
name="filesystem"
|
|
||||||
:label="t('harvester.tab.filesystem')"
|
|
||||||
:weight="-9"
|
|
||||||
>
|
|
||||||
<Filesystem
|
|
||||||
v-model:value="filesystemRows"
|
|
||||||
:mode="isCreate ? mode : 'view'"
|
|
||||||
:namespace="value.metadata.namespace"
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
name="labels"
|
name="labels"
|
||||||
:label="t('generic.labels')"
|
:label="t('generic.labels')"
|
||||||
:weight="-10"
|
:weight="-9"
|
||||||
>
|
>
|
||||||
<Banner color="info">
|
<Banner color="info">
|
||||||
<t k="harvester.virtualMachine.labels.banner" />
|
<t k="harvester.virtualMachine.labels.banner" />
|
||||||
@ -869,7 +787,7 @@ export default {
|
|||||||
<Tab
|
<Tab
|
||||||
name="instanceLabel"
|
name="instanceLabel"
|
||||||
:label="t('harvester.tab.instanceLabel')"
|
:label="t('harvester.tab.instanceLabel')"
|
||||||
:weight="-11"
|
:weight="-10"
|
||||||
>
|
>
|
||||||
<Banner color="info">
|
<Banner color="info">
|
||||||
<t k="harvester.virtualMachine.instanceLabels.banner" />
|
<t k="harvester.virtualMachine.instanceLabels.banner" />
|
||||||
@ -890,7 +808,7 @@ export default {
|
|||||||
<Tab
|
<Tab
|
||||||
name="annotations"
|
name="annotations"
|
||||||
:label="t('harvester.tab.annotations')"
|
:label="t('harvester.tab.annotations')"
|
||||||
:weight="-12"
|
:weight="-11"
|
||||||
>
|
>
|
||||||
<Banner color="info">
|
<Banner color="info">
|
||||||
<t k="harvester.virtualMachine.annotations.banner" />
|
<t k="harvester.virtualMachine.annotations.banner" />
|
||||||
@ -911,7 +829,7 @@ export default {
|
|||||||
<Tab
|
<Tab
|
||||||
name="advanced"
|
name="advanced"
|
||||||
:label="t('harvester.tab.advanced')"
|
:label="t('harvester.tab.advanced')"
|
||||||
:weight="-13"
|
:weight="-12"
|
||||||
>
|
>
|
||||||
<div class="row mb-20">
|
<div class="row mb-20">
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
@ -952,16 +870,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"
|
||||||
|
|||||||
@ -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,419 +0,0 @@
|
|||||||
<script>
|
|
||||||
import CruResource from '@shell/components/CruResource';
|
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|
||||||
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
|
||||||
import InfoBox from '@shell/components/InfoBox';
|
|
||||||
import MessageLink from '@shell/components/MessageLink';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
|
||||||
import HarvesterNodeSelector from '../components/HarvesterNodeSelector';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
|
||||||
import { set } from '@shell/utils/object';
|
|
||||||
import { NODE } from '@shell/config/types';
|
|
||||||
import { HCI } from '../types';
|
|
||||||
import { ADD_ONS } from '../config/harvester-map';
|
|
||||||
|
|
||||||
const MODE_DHCP = 'dhcp';
|
|
||||||
const MODE_STATIC = 'static';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterHostNetworkConfigEditPage',
|
|
||||||
|
|
||||||
emits: ['update:value'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
CruResource,
|
|
||||||
NameNsDescription,
|
|
||||||
ResourceTabs,
|
|
||||||
Tab,
|
|
||||||
InfoBox,
|
|
||||||
MessageLink,
|
|
||||||
LabeledSelect,
|
|
||||||
LabeledInput,
|
|
||||||
RadioGroup,
|
|
||||||
Checkbox,
|
|
||||||
HarvesterNodeSelector,
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
|
||||||
|
|
||||||
inheritAttrs: false,
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
await allHash({
|
|
||||||
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
|
|
||||||
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
|
|
||||||
hostNetworkConfigs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HOST_NETWORK_CONFIG }),
|
|
||||||
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
const networkMode = this.value?.spec?.mode || MODE_DHCP;
|
|
||||||
const ips = { ...(this.value?.spec?.ips || {}) };
|
|
||||||
|
|
||||||
if (!this.value.spec) {
|
|
||||||
set(this.value, 'spec', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
networkMode,
|
|
||||||
ips,
|
|
||||||
hasNodeSelector: !!this.value?.spec?.nodeSelector,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
modeOptions() {
|
|
||||||
return [
|
|
||||||
{ label: 'DHCP', value: MODE_DHCP },
|
|
||||||
{ label: 'Static', value: MODE_STATIC },
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
clusterNetworkOptions() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
|
||||||
|
|
||||||
return clusterNetworks.map((n) => {
|
|
||||||
const disabled = !n.isReady;
|
|
||||||
|
|
||||||
return {
|
|
||||||
label: disabled ? `${ n.id } (${ this.t('generic.notReady') })` : n.id,
|
|
||||||
value: n.id,
|
|
||||||
disabled,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
nodes() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
return this.$store.getters[`${ inStore }/all`](NODE) || [];
|
|
||||||
},
|
|
||||||
|
|
||||||
isStaticMode() {
|
|
||||||
return this.networkMode === MODE_STATIC;
|
|
||||||
},
|
|
||||||
|
|
||||||
underlay: {
|
|
||||||
get() {
|
|
||||||
return !!this.value?.spec?.underlay;
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
set(this.value, 'spec.underlay', val);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
vlanID: {
|
|
||||||
get() {
|
|
||||||
return this.value?.spec?.vlanID;
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
set(this.value, 'spec.vlanID', val);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
clusterNetwork: {
|
|
||||||
get() {
|
|
||||||
return this.value?.spec?.clusterNetwork;
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
set(this.value, 'spec.clusterNetwork', val);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
underlayConflict() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const all = this.$store.getters[`${ inStore }/all`](HCI.HOST_NETWORK_CONFIG) || [];
|
|
||||||
const currentId = this.value?.id;
|
|
||||||
|
|
||||||
return all.find((c) => c.id !== currentId && c.spec?.underlay === true) || null;
|
|
||||||
},
|
|
||||||
|
|
||||||
kubeovnEnabled() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const addons = this.$store.getters[`${ inStore }/all`](HCI.ADD_ONS) || [];
|
|
||||||
|
|
||||||
return addons.find((a) => a.name === ADD_ONS.KUBEOVN_OPERATOR)?.spec?.enabled === true;
|
|
||||||
},
|
|
||||||
|
|
||||||
underlayDisabled() {
|
|
||||||
return !this.kubeovnEnabled || !!this.underlayConflict;
|
|
||||||
},
|
|
||||||
|
|
||||||
kubeovnAddonTo() {
|
|
||||||
return {
|
|
||||||
name: 'c-cluster-product-resource-namespace-id',
|
|
||||||
params: {
|
|
||||||
cluster: this.$route.params.cluster,
|
|
||||||
product: this.$store.getters['productId'],
|
|
||||||
resource: HCI.ADD_ONS,
|
|
||||||
namespace: 'kube-system',
|
|
||||||
id: ADD_ONS.KUBEOVN_OPERATOR,
|
|
||||||
},
|
|
||||||
query: { mode: 'edit' },
|
|
||||||
hash: '#basic',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
networkMode(neu) {
|
|
||||||
set(this.value, 'spec.mode', neu);
|
|
||||||
|
|
||||||
if (neu !== MODE_STATIC) {
|
|
||||||
if (this.value?.spec?.ips !== undefined) {
|
|
||||||
delete this.value.spec.ips;
|
|
||||||
}
|
|
||||||
this.ips = {};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
if (this.registerBeforeHook) {
|
|
||||||
this.registerBeforeHook(this.updateBeforeSave);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
updateBeforeSave() {
|
|
||||||
set(this.value, 'spec.mode', this.networkMode);
|
|
||||||
|
|
||||||
if (this.isStaticMode) {
|
|
||||||
set(this.value, 'spec.ips', { ...this.ips });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateIp(nodeName, val) {
|
|
||||||
this.ips = { ...this.ips, [nodeName]: val };
|
|
||||||
},
|
|
||||||
|
|
||||||
addNodeSelector() {
|
|
||||||
set(this.value.spec, 'nodeSelector', {
|
|
||||||
matchExpressions: [{
|
|
||||||
key: '', operator: 'In', values: []
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
this.hasNodeSelector = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
removeNodeSelector() {
|
|
||||||
delete this.value.spec.nodeSelector;
|
|
||||||
this.hasNodeSelector = false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CruResource
|
|
||||||
:done-route="doneRoute"
|
|
||||||
:mode="mode"
|
|
||||||
:resource="value"
|
|
||||||
:errors="errors"
|
|
||||||
:apply-hooks="applyHooks"
|
|
||||||
@finish="save"
|
|
||||||
@cancel="done"
|
|
||||||
@error="e => errors = e"
|
|
||||||
>
|
|
||||||
<NameNsDescription
|
|
||||||
:value="value"
|
|
||||||
:mode="mode"
|
|
||||||
:namespaced="false"
|
|
||||||
description-key="spec.description"
|
|
||||||
@update:value="$emit('update:value', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ResourceTabs
|
|
||||||
class="mt-15"
|
|
||||||
:need-conditions="false"
|
|
||||||
:need-related="false"
|
|
||||||
:need-events="false"
|
|
||||||
:side-tabs="true"
|
|
||||||
:mode="mode"
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
name="basic"
|
|
||||||
:label="t('harvester.hostNetworkConfig.tabs.mode')"
|
|
||||||
:weight="99"
|
|
||||||
class="bordered-table"
|
|
||||||
>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<RadioGroup
|
|
||||||
v-model:value="networkMode"
|
|
||||||
name="hostNetworkConfigMode"
|
|
||||||
:options="modeOptions"
|
|
||||||
:mode="mode"
|
|
||||||
:row="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="clusterNetwork"
|
|
||||||
:label="t('harvester.network.clusterNetwork.label')"
|
|
||||||
:options="clusterNetworkOptions"
|
|
||||||
:mode="mode"
|
|
||||||
required
|
|
||||||
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value.number="vlanID"
|
|
||||||
type="number"
|
|
||||||
required
|
|
||||||
:min="2"
|
|
||||||
:max="4094"
|
|
||||||
placeholder="e.g. 2 ~ 4094"
|
|
||||||
:label="t('harvester.hostNetworkConfig.vlanID.label')"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<Checkbox
|
|
||||||
v-model:value="underlay"
|
|
||||||
:label="t('harvester.hostNetworkConfig.underlay.label')"
|
|
||||||
:tooltip="t('harvester.hostNetworkConfig.underlay.tooltip')"
|
|
||||||
:mode="mode"
|
|
||||||
:disabled="underlayDisabled"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-if="!kubeovnEnabled"
|
|
||||||
class="underlay-conflict-warning"
|
|
||||||
>
|
|
||||||
<i class="icon icon-warning" />
|
|
||||||
<MessageLink
|
|
||||||
:to="kubeovnAddonTo"
|
|
||||||
prefix-label="harvester.hostNetworkConfig.underlay.noKubeovn.prefix"
|
|
||||||
middle-label="harvester.hostNetworkConfig.underlay.noKubeovn.middle"
|
|
||||||
suffix-label="harvester.hostNetworkConfig.underlay.noKubeovn.suffix"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-else-if="underlayConflict"
|
|
||||||
class="underlay-conflict-warning"
|
|
||||||
>
|
|
||||||
<i class="icon icon-warning" />
|
|
||||||
{{ t('harvester.hostNetworkConfig.underlay.conflict', { name: underlayConflict.nameDisplay || underlayConflict.id }) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-if="isStaticMode">
|
|
||||||
<hr class="section-divider" />
|
|
||||||
<div
|
|
||||||
v-for="node in nodes"
|
|
||||||
:key="node.id"
|
|
||||||
class="row mb-10 ips-row"
|
|
||||||
>
|
|
||||||
<div class="col span-3">
|
|
||||||
<LabeledInput
|
|
||||||
:value="node.nameDisplay || node.id"
|
|
||||||
:label="t('harvester.hostNetworkConfig.ips.nodeLabel')"
|
|
||||||
mode="view"
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-5">
|
|
||||||
<LabeledInput
|
|
||||||
:value="ips[node.id]"
|
|
||||||
:label="t('harvester.hostNetworkConfig.ips.label')"
|
|
||||||
:placeholder="t('harvester.hostNetworkConfig.ips.placeholder')"
|
|
||||||
:mode="mode"
|
|
||||||
required
|
|
||||||
@update:value="updateIp(node.id, $event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
name="nodeSelector"
|
|
||||||
:label="t('harvester.hostNetworkConfig.tabs.nodeSelector')"
|
|
||||||
:weight="98"
|
|
||||||
>
|
|
||||||
<template v-if="hasNodeSelector">
|
|
||||||
<InfoBox class="node-selector-box">
|
|
||||||
<button
|
|
||||||
v-if="!isView"
|
|
||||||
type="button"
|
|
||||||
class="role-link btn btn-sm remove"
|
|
||||||
:aria-label="t('generic.remove')"
|
|
||||||
@click="removeNodeSelector"
|
|
||||||
>
|
|
||||||
<i class="icon icon-x" />
|
|
||||||
</button>
|
|
||||||
<HarvesterNodeSelector
|
|
||||||
class="mt-20"
|
|
||||||
:value="value.spec.nodeSelector"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</InfoBox>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn role-secondary"
|
|
||||||
:disabled="isView"
|
|
||||||
@click="addNodeSelector"
|
|
||||||
>
|
|
||||||
{{ t('harvester.hostNetworkConfig.nodeSelector.addButton') }}
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</Tab>
|
|
||||||
</ResourceTabs>
|
|
||||||
</CruResource>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.section-divider {
|
|
||||||
border: none;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
margin: 10px 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-selector-box {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.remove {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
z-index: 1;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.underlay-conflict-warning {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
margin-top: 4px;
|
|
||||||
color: var(--warning);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -31,10 +31,6 @@ export default {
|
|||||||
to() {
|
to() {
|
||||||
return this.vm?.detailLocation;
|
return this.vm?.detailLocation;
|
||||||
},
|
},
|
||||||
|
|
||||||
attachVMName() {
|
|
||||||
return this.vm?.nameDisplay || this.vm?.metadata?.name || this.value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -44,10 +40,10 @@ export default {
|
|||||||
v-if="to"
|
v-if="to"
|
||||||
:to="to"
|
:to="to"
|
||||||
>
|
>
|
||||||
{{ attachVMName }}
|
{{ value }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ attachVMName }}
|
{{ value }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -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: [],
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterBooleanFormatter',
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<span v-if="value">
|
|
||||||
<i class="icon icon-checkmark" />
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-else
|
|
||||||
class="text-muted"
|
|
||||||
>
|
|
||||||
—
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterHostNetworkConfigModeFormatter',
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
displayMode() {
|
|
||||||
if (this.value?.toLowerCase() === 'dhcp') {
|
|
||||||
return 'DHCP';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.value?.toLowerCase() === 'static') {
|
|
||||||
return 'Static';
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.value;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<span>{{ displayMode }}</span>
|
|
||||||
</template>
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user