mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-05-14 06:51:46 +00:00
Compare commits
123 Commits
v1.7.2-rc1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9de065a5c9 | ||
|
|
cd933bdbf8 | ||
|
|
5fe642a42d | ||
|
|
18c66083ab | ||
|
|
d291a35754 | ||
|
|
5cc8b4c301 | ||
|
|
032700293c | ||
|
|
8cb793e7ad | ||
|
|
2c45b71d1f | ||
|
|
5a301dcf55 | ||
|
|
1a92265d03 | ||
|
|
8f65915bad | ||
|
|
67bb6dfbd5 | ||
|
|
e74428d951 | ||
|
|
629f7df6b9 | ||
|
|
b7119d5c4c | ||
|
|
6fdd1e3954 | ||
|
|
7d0f33f31d | ||
|
|
9ce95daf76 | ||
|
|
ce72232bc3 | ||
|
|
afc0e0f531 | ||
|
|
e941cc9a90 | ||
|
|
9961523d08 | ||
|
|
c4d1018388 | ||
|
|
be64329776 | ||
|
|
35411ed87a | ||
|
|
15eb0f07f7 | ||
|
|
fb78f24fdd | ||
|
|
81ad827829 | ||
|
|
6dd9b33336 | ||
|
|
1f9e9b336b | ||
|
|
c5b4f6cd1e | ||
|
|
4ce35ce075 | ||
|
|
27c26bd782 | ||
|
|
9d698b1230 | ||
|
|
566e79eda5 | ||
|
|
42ddcfc1fe | ||
|
|
ad3decf71f | ||
|
|
8083a41df0 | ||
|
|
46b860260a | ||
|
|
62801b3b13 | ||
|
|
161e3bbd97 | ||
|
|
97e93dba0b | ||
|
|
9a8a709e56 | ||
|
|
d1949641a7 | ||
|
|
9c9f59c939 | ||
|
|
ccc14c7fb9 | ||
|
|
2ba471907e | ||
|
|
5aea476f64 | ||
|
|
519c7d9f1f | ||
|
|
a9c392c13f | ||
|
|
888ec7a50f | ||
|
|
a2486a7d38 | ||
|
|
df3d249923 | ||
|
|
23344e0c07 | ||
|
|
62b80b3cec | ||
|
|
1cf94ee550 | ||
|
|
dfaa9fbe33 | ||
|
|
94b1c24479 | ||
|
|
e0b2b9ec57 | ||
|
|
06b38a0f99 | ||
|
|
4be3634c82 | ||
|
|
de103ff91e | ||
|
|
37a91601c9 | ||
|
|
9794671ccd | ||
|
|
337f9186f1 | ||
|
|
7c187e894d | ||
|
|
c2df13ad73 | ||
|
|
93f027a57c | ||
|
|
da83f04b6c | ||
|
|
a97cb08e3f | ||
|
|
3fdc9f03a3 | ||
|
|
c8a613874a | ||
|
|
2db7ee7397 | ||
|
|
8b9b5b41b7 | ||
|
|
77599900b5 | ||
|
|
473c1ba355 | ||
|
|
708a95b67b | ||
|
|
fecb3de0cf | ||
|
|
0781bde188 | ||
|
|
0647600e88 | ||
|
|
99dbba7958 | ||
|
|
3dcc50980b | ||
|
|
ee1c3de188 | ||
|
|
915559962a | ||
|
|
b1b1a31c04 | ||
|
|
7f52562d22 | ||
|
|
b140c05697 | ||
|
|
ad9fef63c0 | ||
|
|
786e271ac6 | ||
|
|
c169853e5a | ||
|
|
1352246e1e | ||
|
|
49374bb18a | ||
|
|
fe3a12e28c | ||
|
|
a86302c9d5 | ||
|
|
5fe7e13fcd | ||
|
|
c079984047 | ||
|
|
5769588633 | ||
|
|
b29950f99c | ||
|
|
6c27a46274 | ||
|
|
b03fffbc30 | ||
|
|
5b668a176c | ||
|
|
b4019a2c86 | ||
|
|
416098ffd8 | ||
|
|
3d7b96d86d | ||
|
|
0b37467f76 | ||
|
|
fab7fbec5e | ||
|
|
d94003f8c2 | ||
|
|
dbb5b01cc3 | ||
|
|
467933bda0 | ||
|
|
1b183febdc | ||
|
|
70d3b656f7 | ||
|
|
10d19cd329 | ||
|
|
87e44cb658 | ||
|
|
1715ae754c | ||
|
|
30de2b1a18 | ||
|
|
6fedcc353c | ||
|
|
f9bff21e84 | ||
|
|
6735826e15 | ||
|
|
9e17e239cf | ||
|
|
a1cf41bda9 | ||
|
|
db58024351 | ||
|
|
81bf19419c |
3
.github/actions/setup/action.yaml
vendored
3
.github/actions/setup/action.yaml
vendored
@ -4,7 +4,8 @@ description: Setup node and install dependencies
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- name: Setup Nodejs with yarn caching
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
6
.github/auto-assign-config.yaml
vendored
6
.github/auto-assign-config.yaml
vendored
@ -1,6 +0,0 @@
|
||||
addAssignees: author
|
||||
addReviewers: true
|
||||
numberOfReviewers: 0
|
||||
reviewers:
|
||||
- a110605
|
||||
- houhoucoop
|
||||
22
.github/renovate.json
vendored
22
.github/renovate.json
vendored
@ -7,7 +7,8 @@
|
||||
],
|
||||
"baseBranches": [
|
||||
"main",
|
||||
"/^release-harvester-v\\d+\\.\\d+$/"
|
||||
"release-harvester-v1.7",
|
||||
"release-harvester-v1.8"
|
||||
],
|
||||
"automergeMajor": false,
|
||||
"semanticCommits": "enabled",
|
||||
@ -33,14 +34,25 @@
|
||||
{
|
||||
"matchUpdateTypes": ["minor"],
|
||||
"groupName": "minor dependencies",
|
||||
"minimumReleaseAge": "7 days",
|
||||
"labels": ["minor-update"],
|
||||
"reviewers": ["a110605", "houhoucoop"]
|
||||
},
|
||||
{
|
||||
"matchUpdateTypes": ["patch", "digest"],
|
||||
"automerge": true,
|
||||
"groupName": "patch digest dependencies",
|
||||
"labels": ["patch-update", "automerge"]
|
||||
"matchUpdateTypes": ["patch"],
|
||||
"automerge": false,
|
||||
"minimumReleaseAge": "7 days",
|
||||
"groupName": "patch dependencies",
|
||||
"labels": ["patch-update"],
|
||||
"reviewers": ["a110605", "houhoucoop"]
|
||||
},
|
||||
{
|
||||
"matchUpdateTypes": ["digest", "pinDigest"],
|
||||
"automerge": false,
|
||||
"groupName": "digest dependencies",
|
||||
"labels": ["digest-update"],
|
||||
"schedule": ["on the first day of the month"],
|
||||
"reviewers": ["a110605", "houhoucoop"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
39
.github/workflows/add-pr-label-check.yaml
vendored
Normal file
39
.github/workflows/add-pr-label-check.yaml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
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
Normal file
33
.github/workflows/add-pr-label.yaml
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
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
Normal file
28
.github/workflows/auto-assign-check.yaml
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
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
17
.github/workflows/auto-assign-reviewer.yaml
vendored
@ -1,17 +0,0 @@
|
||||
name: "[PR Management] Auto Assign Reviewer & Assignee"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, ready_for_review]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
auto-assign:
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: rancher/gh-issue-mgr/auto-assign-action@main
|
||||
with:
|
||||
configuration-path: .github/auto-assign-config.yaml
|
||||
35
.github/workflows/auto-assign.yaml
vendored
Normal file
35
.github/workflows/auto-assign.yaml
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
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
Normal file
30
.github/workflows/backport-label-check.yaml
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
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,40 +1,44 @@
|
||||
name: "[PR Management] Add Labels"
|
||||
name: "PR Management Add Backport Labels"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
branches:
|
||||
- main
|
||||
- 'release-harvester-v*'
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
workflow_run:
|
||||
workflows:
|
||||
- "PR Management Add Labels Collect Data"
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
add-require-backport-label:
|
||||
if: github.event.pull_request.draft == false &&
|
||||
github.event.pull_request.base.ref == 'main'
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
permissions:
|
||||
actions: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Download PR data artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
name: pr-backport-label-data
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Fetch release branches and PR labels
|
||||
id: fetch_info
|
||||
- name: Load PR data
|
||||
run: |
|
||||
cat pr-backport-label-data.env >> $GITHUB_ENV
|
||||
|
||||
- name: Add require-backport label (main branch PRs)
|
||||
if: env.PR_BASE_REF == 'main'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
repo="${{ github.repository }}"
|
||||
pr_number=${{ github.event.pull_request.number }}
|
||||
pr_number="$PR_NUMBER"
|
||||
|
||||
release_branches=$(gh api "repos/${repo}/branches" --paginate --jq '.[].name' | grep -E '^release-harvester-v[0-9]+\.[0-9]+$' || true)
|
||||
|
||||
if [[ -z "$release_branches" ]]; then
|
||||
echo "should_label=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No release branches found, skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@ -44,51 +48,36 @@ jobs:
|
||||
|
||||
tags=$(gh api "repos/${repo}/releases" --paginate --jq '.[].tag_name')
|
||||
if echo "$tags" | grep -Fxq "$release_tag"; then
|
||||
echo "should_label=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Release $release_tag already published, skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
label="require backport/v${version}"
|
||||
echo "should_label=true" >> "$GITHUB_OUTPUT"
|
||||
echo "backport_label=$label" >> "$GITHUB_OUTPUT"
|
||||
|
||||
pr_labels=$(gh pr view "$pr_number" --repo "$repo" --json labels --jq '.labels[].name' || echo "")
|
||||
pr_labels_csv=$(echo "$pr_labels" | tr '\n' ',' | sed 's/,$//')
|
||||
echo "pr_labels=$pr_labels_csv" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Add label if needed
|
||||
if: steps.fetch_info.outputs.should_label == 'true' && !contains(steps.fetch_info.outputs.pr_labels, steps.fetch_info.outputs.backport_label)
|
||||
if echo "$pr_labels" | grep -Fxq "$label"; then
|
||||
echo "Label '$label' already present, skipping."
|
||||
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:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
echo "Adding label: ${{ steps.fetch_info.outputs.backport_label }}"
|
||||
gh pr edit ${{ github.event.pull_request.number }} \
|
||||
--repo ${{ github.repository }} \
|
||||
--add-label "${{ steps.fetch_info.outputs.backport_label }}"
|
||||
set -euo pipefail
|
||||
|
||||
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 }}
|
||||
IS_MERGIFY=$(echo "$PR_USER_LOGIN" | grep -iq 'mergify' && echo true || echo false)
|
||||
|
||||
echo "IS_MERGIFY=$IS_MERGIFY" >> $GITHUB_OUTPUT
|
||||
echo "TARGET_BRANCH=$TARGET_BRANCH" >> $GITHUB_OUTPUT
|
||||
if [[ "$IS_MERGIFY" != "true" ]]; then
|
||||
echo "PR author is not Mergify, skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
- 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}"
|
||||
version="${PR_BASE_REF#release-harvester-v}"
|
||||
label="backport/v${version}"
|
||||
echo "Adding label $label"
|
||||
gh pr edit ${{ github.event.pull_request.number }} \
|
||||
--repo ${{ github.repository }} \
|
||||
--add-label "$label"
|
||||
echo "Adding label: $label"
|
||||
gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label "$label"
|
||||
|
||||
28
.github/workflows/backport-request-check.yaml
vendored
Normal file
28
.github/workflows/backport-request-check.yaml
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
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,41 +1,47 @@
|
||||
name: "[PR Management] Request Backport via Mergify"
|
||||
name: "PR Management Request Backport via Mergify"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
workflow_run:
|
||||
workflows:
|
||||
- "PR Management Request Backport via Mergify Collect Data"
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
comment-backport:
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
permissions:
|
||||
actions: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Download PR data artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
name: pr-backport-request-data
|
||||
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
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
pr_number=${{ github.event.pull_request.number }}
|
||||
pr_number="$PR_NUMBER"
|
||||
repo="${{ github.repository }}"
|
||||
|
||||
labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
|
||||
labels=$(echo "$labels_json" | jq -r '.[] // empty')
|
||||
labels=$(echo "$PR_LABELS" | tr ',' '\n')
|
||||
|
||||
echo "Labels on PR: $labels"
|
||||
|
||||
matches=$(echo "$labels" | grep -oE '^require backport/v[0-9]+\.[0-9]+$' || true)
|
||||
|
||||
if [[ -z "$matches" ]]; then
|
||||
echo "No back‑port labels found — skipping."
|
||||
echo "No back-port labels found — skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Check package version
|
||||
env:
|
||||
|
||||
@ -25,12 +25,12 @@ jobs:
|
||||
name: Build & Upload Hosted
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
@ -45,19 +45,20 @@ jobs:
|
||||
run: ./scripts/build-upload-gate
|
||||
|
||||
- name: Get gcs auth
|
||||
uses: rancher-eio/read-vault-secrets@main
|
||||
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
||||
with:
|
||||
secrets: |
|
||||
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
||||
|
||||
- name: Apply gcs auth
|
||||
# https://github.com/google-github-actions/auth
|
||||
uses: 'google-github-actions/auth@v2'
|
||||
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
||||
|
||||
with:
|
||||
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
||||
|
||||
- name: Upload build
|
||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
||||
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
||||
# https://github.com/google-github-actions/upload-cloud-storage
|
||||
with:
|
||||
path: ${{steps.build-hosted.outputs.BUILD_HOSTED_DIR}}
|
||||
@ -71,12 +72,12 @@ jobs:
|
||||
name: Build & Upload Embedded
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
@ -89,19 +90,19 @@ jobs:
|
||||
DISABLED_EMBED_PKG: https://releases.rancher.com/harvester-ui/plugin/harvester-1.0.3.tar.gz
|
||||
|
||||
- name: Get gcs auth
|
||||
uses: rancher-eio/read-vault-secrets@main
|
||||
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
||||
with:
|
||||
secrets: |
|
||||
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
||||
|
||||
- name: Apply gcs auth
|
||||
# https://github.com/google-github-actions/auth
|
||||
uses: 'google-github-actions/auth@v2'
|
||||
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
||||
with:
|
||||
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
||||
|
||||
- name: Upload tar
|
||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
||||
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
||||
with:
|
||||
path: ${{steps.build-embedded.outputs.BUILD_EMBEDED_TGZ}}
|
||||
destination: releases.rancher.com/harvester-ui/dashboard
|
||||
@ -114,12 +115,12 @@ jobs:
|
||||
name: Build & Upload Plugin
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
@ -133,19 +134,19 @@ jobs:
|
||||
run: ./scripts/build-upload-gate
|
||||
|
||||
- name: Get gcs auth
|
||||
uses: rancher-eio/read-vault-secrets@main
|
||||
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
||||
with:
|
||||
secrets: |
|
||||
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
||||
|
||||
- name: Apply gcs auth
|
||||
# https://github.com/google-github-actions/auth
|
||||
uses: 'google-github-actions/auth@v2'
|
||||
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
||||
with:
|
||||
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
||||
|
||||
- name: Upload plugin tar
|
||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
||||
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
||||
with:
|
||||
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_TARBALL}}
|
||||
destination: releases.rancher.com/harvester-ui/plugin
|
||||
@ -155,7 +156,7 @@ jobs:
|
||||
process_gcloudignore: false
|
||||
|
||||
- name: Upload plugin directory
|
||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
||||
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
||||
with:
|
||||
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_NAME}}
|
||||
destination: releases.rancher.com/harvester-ui/plugin/${{steps.ci-build-pkg.outputs.PKG_NAME}}
|
||||
|
||||
20
.github/workflows/build-extension-catalog.yml
vendored
20
.github/workflows/build-extension-catalog.yml
vendored
@ -27,14 +27,14 @@ jobs:
|
||||
build-status: ${{ job.status }}
|
||||
steps:
|
||||
- name: Read Secrets
|
||||
uses: rancher-eio/read-vault-secrets@main
|
||||
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
||||
with:
|
||||
secrets: |
|
||||
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | DOCKER_USERNAME ;
|
||||
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD ;
|
||||
|
||||
- name: Checkout repository (normal flow)
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
@ -45,26 +45,20 @@ jobs:
|
||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
username: ${{ env.DOCKER_USERNAME }}
|
||||
password: ${{ env.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Setup Helm
|
||||
uses: azure/setup-helm@v3
|
||||
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
|
||||
with:
|
||||
version: v3.8.0
|
||||
|
||||
- name: Setup Nodejs with yarn caching
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
- name: Setup Nodejs with yarn install
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build and push UI image
|
||||
run: |
|
||||
publish="yarn publish-pkgs -cp -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
|
||||
$publish
|
||||
$publish
|
||||
|
||||
24
.github/workflows/build-extension-on-merge.yml
vendored
24
.github/workflows/build-extension-on-merge.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
target_branch: ${{ steps.get-version.outputs.target_branch }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Determine target branch
|
||||
id: get-version
|
||||
@ -44,7 +44,7 @@ jobs:
|
||||
version: ${{ steps.get_version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Extract version from package.json
|
||||
id: get_version
|
||||
@ -62,7 +62,13 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@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
|
||||
run: |
|
||||
@ -70,7 +76,7 @@ jobs:
|
||||
yarn install --frozen-lockfile
|
||||
|
||||
- name: Setup Helm
|
||||
uses: azure/setup-helm@v3
|
||||
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
|
||||
with:
|
||||
version: v3.8.0
|
||||
|
||||
@ -79,7 +85,7 @@ jobs:
|
||||
yarn publish-pkgs -s ${{ github.repository }} -b ${{ needs.setup-target-branch.outputs.target_branch }} -t harvester-${{ needs.extract-version.outputs.version }}
|
||||
|
||||
- name: Upload charts artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: charts
|
||||
path: tmp
|
||||
@ -94,7 +100,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout release branch
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
ref: '${{ github.ref_name }}'
|
||||
|
||||
@ -105,7 +111,7 @@ jobs:
|
||||
echo "LAST_COMMIT=${LAST_COMMIT}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout target branch
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
ref: '${{ needs.setup-target-branch.outputs.target_branch }}'
|
||||
|
||||
@ -121,7 +127,7 @@ jobs:
|
||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: charts
|
||||
|
||||
@ -132,7 +138,7 @@ jobs:
|
||||
git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
|
||||
|
||||
- name: Run Helm chart releaser
|
||||
uses: helm/chart-releaser-action@v1.7.0
|
||||
uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0
|
||||
with:
|
||||
charts_dir: ./charts
|
||||
env:
|
||||
|
||||
@ -17,7 +17,7 @@ jobs:
|
||||
release_tag: ${{ steps.determine_tag.outputs.release_tag }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Determine release tag
|
||||
id: determine_tag
|
||||
@ -33,7 +33,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Check package version
|
||||
env:
|
||||
TAG_VERSION: ${{ github.event.release.tag_name }}
|
||||
@ -43,7 +43,7 @@ jobs:
|
||||
needs:
|
||||
- setup-release-tag
|
||||
- check-version
|
||||
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
|
||||
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@3b26a36bad555e5e2b8634b24823be29732f287c # master
|
||||
permissions:
|
||||
actions: write
|
||||
contents: write
|
||||
|
||||
@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Check package version
|
||||
env:
|
||||
TAG_VERSION: ${{github.ref_name}}
|
||||
|
||||
34
.github/workflows/fossa.yml
vendored
Normal file
34
.github/workflows/fossa.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
name: FOSSA Scanning
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main", "release-harvester-v*"]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
fossa-scanning:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
# The FOSSA token is shared between all repos in Harvester's GH org. It can
|
||||
# be used directly and there is no need to request specific access to EIO.
|
||||
- name: Read FOSSA token
|
||||
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
||||
with:
|
||||
secrets: |
|
||||
secret/data/github/org/harvester/fossa/credentials token | FOSSA_API_KEY_PUSH_ONLY
|
||||
|
||||
- name: FOSSA scan
|
||||
uses: fossas/fossa-action@ff70fe9fe17cbd2040648f1c45e8ec4e4884dcf3 # v1.9.0
|
||||
with:
|
||||
api-key: ${{ env.FOSSA_API_KEY_PUSH_ONLY }}
|
||||
# Only runs the scan and do not provide/returns any results back to the
|
||||
# pipeline.
|
||||
run-tests: false
|
||||
30
.github/workflows/release-label.yaml
vendored
30
.github/workflows/release-label.yaml
vendored
@ -1,30 +0,0 @@
|
||||
name: "[PR Management] Add PR Label"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, edited]
|
||||
branches:
|
||||
- main
|
||||
- 'release-harvester-v*'
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
auto-assign-pr-label:
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
- name: Setup Nodejs and yarn install
|
||||
uses: ./.github/actions/setup
|
||||
- name: Set PR label
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
|
||||
echo "PR_LABEL = $PR_LABEL"
|
||||
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "$PR_LABEL"
|
||||
23
.github/workflows/run-lint.yaml
vendored
23
.github/workflows/run-lint.yaml
vendored
@ -1,7 +1,7 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
workflow_call: # This tells GH that the workflow is reusable
|
||||
workflow_call: # This tells GH that the workflow is reusable
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
@ -16,7 +16,7 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0 # Need full history for commit-lint
|
||||
|
||||
@ -41,8 +41,14 @@ jobs:
|
||||
FROM="$GITHUB_BASE_SHA"
|
||||
TO="$GITHUB_HEAD_SHA"
|
||||
elif [ -n "$GITHUB_BEFORE" ] && [ -n "$GITHUB_AFTER" ]; then
|
||||
FROM="$GITHUB_BEFORE"
|
||||
TO="$GITHUB_AFTER"
|
||||
if [ "$GITHUB_BEFORE" = "0000000000000000000000000000000000000000" ]; then
|
||||
# first push to HEAD
|
||||
FROM=""
|
||||
TO="$GITHUB_AFTER"
|
||||
else
|
||||
FROM="$GITHUB_BEFORE"
|
||||
TO="$GITHUB_AFTER"
|
||||
fi
|
||||
else
|
||||
echo "No valid commit range found, skipping commitlint."
|
||||
exit 0
|
||||
@ -51,7 +57,14 @@ jobs:
|
||||
echo "FROM=$FROM"
|
||||
echo "TO=$TO"
|
||||
|
||||
npx commitlint --from "$FROM" --to "$TO" --verbose
|
||||
if [ -z "$FROM" ]; then
|
||||
echo "Linting last commit $TO"
|
||||
npx commitlint --last --verbose
|
||||
|
||||
else
|
||||
echo "Linting commits from $FROM to $TO"
|
||||
npx commitlint --from "$FROM" --to "$TO" --verbose
|
||||
fi
|
||||
env:
|
||||
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
||||
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
261
AGENTS.md
Normal file
261
AGENTS.md
Normal file
@ -0,0 +1,261 @@
|
||||
> 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
|
||||
|
||||
2
CODEOWNERS
Normal file
2
CODEOWNERS
Normal file
@ -0,0 +1,2 @@
|
||||
@a110605
|
||||
@houhoucoop
|
||||
@ -7,7 +7,7 @@ The Harvester UI Extension is a Rancher extension that provides the user interfa
|
||||
|
||||
## Installation
|
||||
|
||||
For detailed installation instructions, please refer to the [official Harvester documentation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210).
|
||||
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
|
||||
|
||||
|
||||
## Development Setup
|
||||
@ -163,7 +163,7 @@ If you want to contribute, start by reading this document, then visit our [Getti
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2014-2025 [SUSE, LLC.](https://www.suse.com/)
|
||||
Copyright (c) 2014-2026 [SUSE, LLC.](https://www.suse.com/)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
63
docs/agents.md/README
Normal file
63
docs/agents.md/README
Normal file
@ -0,0 +1,63 @@
|
||||
# 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
|
||||
23
docs/agents.md/agents/boundaries.md
Normal file
23
docs/agents.md/agents/boundaries.md
Normal file
@ -0,0 +1,23 @@
|
||||
## 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.
|
||||
65
docs/agents.md/agents/tools.md
Normal file
65
docs/agents.md/agents/tools.md
Normal file
@ -0,0 +1,65 @@
|
||||
## 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
|
||||
```
|
||||
70
docs/agents.md/contributors/1_contributors.md
Normal file
70
docs/agents.md/contributors/1_contributors.md
Normal file
@ -0,0 +1,70 @@
|
||||
## 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.
|
||||
|
||||
3
docs/agents.md/contributors/2_e2e-tests.md
Normal file
3
docs/agents.md/contributors/2_e2e-tests.md
Normal file
@ -0,0 +1,3 @@
|
||||
## E2E Tests (Cypress)
|
||||
|
||||
See https://github.com/harvester/harvester-ui-tests
|
||||
12
docs/agents.md/contributors/node-dependencies.md
Normal file
12
docs/agents.md/contributors/node-dependencies.md
Normal file
@ -0,0 +1,12 @@
|
||||
## 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`.
|
||||
|
||||
58
docs/agents.md/contributors/resolving_gh_issues.md
Normal file
58
docs/agents.md/contributors/resolving_gh_issues.md
Normal file
@ -0,0 +1,58 @@
|
||||
## 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
|
||||
5
docs/agents.md/personas/software_developer.md
Normal file
5
docs/agents.md/personas/software_developer.md
Normal file
@ -0,0 +1,5 @@
|
||||
## 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.
|
||||
22
docs/agents.md/template_agents.md
Normal file
22
docs/agents.md/template_agents.md
Normal file
@ -0,0 +1,22 @@
|
||||
> 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>`
|
||||
31
package.json
31
package.json
@ -1,36 +1,41 @@
|
||||
{
|
||||
"name": "harvester-ui-extension",
|
||||
"version": "1.7.0-dev",
|
||||
"version": "1.9.0-dev",
|
||||
"private": false,
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
"node": ">=24.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-class-static-block": "7.28.3",
|
||||
"@rancher/shell": "3.0.5-rc.8",
|
||||
"@babel/plugin-transform-class-static-block": "7.28.6",
|
||||
"@rancher/shell": "3.0.12-rc.1",
|
||||
"@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",
|
||||
"color": "4.2.3",
|
||||
"ip": "2.0.1",
|
||||
"node-polyfill-webpack-plugin": "^3.0.0",
|
||||
"elkjs": "^0.11.0",
|
||||
"vue-draggable-next": "^2.2.1",
|
||||
"yaml": "^2.5.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "~20.19.0",
|
||||
"@types/node": "25.6.0",
|
||||
"cronstrue": "2.59.0",
|
||||
"d3-color": "3.1.0",
|
||||
"ejs": "3.1.10",
|
||||
"follow-redirects": "1.15.11",
|
||||
"follow-redirects": "1.16.0",
|
||||
"glob": "7.2.3",
|
||||
"glob-parent": "6.0.2",
|
||||
"json5": "2.2.3",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/lodash": "4.17.24",
|
||||
"merge": "2.1.1",
|
||||
"node-forge": "1.3.1",
|
||||
"node-forge": "1.4.0",
|
||||
"nth-check": "2.1.1",
|
||||
"qs": "6.14.0",
|
||||
"roarr": "7.21.2",
|
||||
"semver": "7.7.3",
|
||||
"qs": "6.15.1",
|
||||
"roarr": "7.21.4",
|
||||
"semver": "7.7.4",
|
||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
@ -38,12 +43,14 @@
|
||||
"build": "./node_modules/.bin/vue-cli-service build",
|
||||
"clean": "./node_modules/@rancher/shell/scripts/clean",
|
||||
"lint": "./node_modules/.bin/eslint --max-warnings 0 --ext .js,.ts,.vue .",
|
||||
"lint:fix": "./node_modules/.bin/eslint --fix --max-warnings 0 --ext .js,.ts,.vue .",
|
||||
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
|
||||
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
|
||||
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
|
||||
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
|
||||
"commitlint": "commitlint --edit",
|
||||
"prepare": "husky"
|
||||
"prepare": "husky",
|
||||
"agents:generate": "./scripts/generate-agent-and-persona-mds.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/load": "^19.8.1",
|
||||
|
||||
177
pkg/harvester/README.md
Normal file
177
pkg/harvester/README.md
Normal file
@ -0,0 +1,177 @@
|
||||
# harvester-ui-extension
|
||||
|
||||
The Harvester UI Extension is a Rancher extension that provides the user interface for [Harvester](https://harvesterhci.io) within the [Rancher Dashboard](https://github.com/rancher/dashboard).
|
||||
|
||||
> **Note:**
|
||||
> This extension is available starting from **Rancher 2.10.0**. Ensure your Rancher version is **2.10.0 or later** to access Harvester integration.
|
||||
|
||||
## Installation
|
||||
|
||||
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
|
||||
|
||||
## Development Setup
|
||||
|
||||
Ensure **Node.js v20 or later** is installed for development and debugging.
|
||||
|
||||
### Standalone Mode
|
||||
|
||||
Run the extension standalone with hot reload at `https://localhost:8005`.
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
yarn install
|
||||
|
||||
# Start the development server
|
||||
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
||||
|
||||
# Example with specific server version
|
||||
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.5.0 API=https://192.168.1.123 yarn dev
|
||||
```
|
||||
|
||||
You may also define environment variables in a `.env` file:
|
||||
|
||||
```env
|
||||
RANCHER_ENV=harvester
|
||||
VUE_APP_SERVER_VERSION=v1.5.0
|
||||
API=https://192.168.1.123
|
||||
```
|
||||
|
||||
### Rancher Integration Mode
|
||||
|
||||
To run as a Rancher extension, follow the [Rancher UI Extension Guide](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
|
||||
|
||||
```bash
|
||||
API=https://your-rancher-ip yarn dev
|
||||
```
|
||||
|
||||
## Commit Message Guidelines
|
||||
|
||||
This project uses [commit-lint](https://commitlint.js.org/) with [Conventional Commits](https://www.conventionalcommits.org/) to ensure consistent and meaningful commit messages.
|
||||
|
||||
### Commit Message Format
|
||||
|
||||
All commit messages must follow the conventional commit format:
|
||||
|
||||
```
|
||||
<type>[optional scope]: <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer(s)]
|
||||
```
|
||||
|
||||
### Supported Types
|
||||
|
||||
- **feat**: New features
|
||||
- **fix**: Bug fixes
|
||||
- **docs**: Documentation changes
|
||||
- **style**: Code style changes (formatting, missing semicolons, etc.)
|
||||
- **refactor**: Code refactoring
|
||||
- **perf**: Performance improvements
|
||||
- **test**: Adding or updating tests
|
||||
- **build**: Build system or external dependencies
|
||||
- **ci**: CI/CD changes
|
||||
- **chore**: Other changes that don't modify src or test files
|
||||
- **revert**: Reverts a previous commit
|
||||
- **wip**: Work in progress
|
||||
- **deps**: Dependency updates
|
||||
- **security**: Security fixes
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Feature
|
||||
git commit -m "feat: add new virtual machine creation wizard"
|
||||
|
||||
# Bug fix
|
||||
git commit -m "fix: resolve memory leak in VM console"
|
||||
|
||||
# Documentation
|
||||
git commit -m "docs: update installation instructions"
|
||||
|
||||
# Breaking change
|
||||
git commit -m "feat!: change API endpoint structure
|
||||
|
||||
BREAKING CHANGE: The /api/v1/vms endpoint has been replaced with /api/v2/vms"
|
||||
```
|
||||
|
||||
### Git Hooks
|
||||
|
||||
The project uses [Husky](https://typicode.github.io/husky/) to automatically validate commit messages and run linting before commits:
|
||||
|
||||
- **pre-commit**: Runs ESLint to ensure code quality
|
||||
- **commit-msg**: Validates commit message format using commit-lint
|
||||
|
||||
These hooks are automatically installed when you run `yarn install`.
|
||||
|
||||
### Manual Validation
|
||||
|
||||
You can manually validate commit messages:
|
||||
|
||||
```bash
|
||||
# Validate the last commit
|
||||
yarn commitlint
|
||||
|
||||
# Validate a specific commit
|
||||
npx commitlint --from <commit-hash>
|
||||
|
||||
# Validate a range of commits
|
||||
npx commitlint --from <start-hash> --to <end-hash>
|
||||
```
|
||||
|
||||
## Branch Structure
|
||||
|
||||
- **`main`** – Main development branch
|
||||
- **`release-harvester-vX.Y`** – Stable release branches per version series
|
||||
- **`vX.Y-head`** – Testing branches for ongoing changes to extension builds in each release series
|
||||
|
||||
> **Note:**
|
||||
> The `vX.Y-head` branches are auto-generated and kept in sync with release branches. Use these for testing the latest changes in each version series.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
### UI Extension Testing
|
||||
|
||||
To validate changes in a release series, switch to the appropriate `vX.Y-head` branch. For main branch testing, use `main-head`.
|
||||
|
||||
- Examples:
|
||||
- Test `1.0.x` series → `v1.0-head`
|
||||
- Test `1.5.x` series → `v1.5-head`
|
||||
|
||||
**Steps:**
|
||||
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
|
||||
2. Refresh the Harvester repository using the target `vX.Y-head` branch
|
||||
3. Go to the **Extensions** page and install the desired version
|
||||
|
||||
### Standalone Mode Testing
|
||||
|
||||
To test the standalone UI, configure Harvester to load the UI from an external source.
|
||||
|
||||
- Examples of `ui-index`:
|
||||
- Main branch → `https://releases.rancher.com/harvester-ui/dashboard/latest/index.html`
|
||||
- Release series `1.5.x` → `https://releases.rancher.com/harvester-ui/dashboard/release-harvester-v1.5/index.html`
|
||||
|
||||
**Steps:**
|
||||
1. Go to **Harvester UI** → **Advanced** → **Settings** → **UI**
|
||||
2. Set **ui-source** to `External`
|
||||
3. Set **ui-index** to the desired URL
|
||||
|
||||
## Contributing
|
||||
|
||||
If you want to contribute, start by reading this document, then visit our [Getting Started guide](https://extensions.rancher.io/extensions/next/extensions-getting-started) to learn how to develop and submit changes.
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2014-2026 [SUSE, LLC.](https://www.suse.com/)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@ -1,6 +1,8 @@
|
||||
<script>
|
||||
import Collapse from '@shell/components/Collapse';
|
||||
import PercentageBar from '@shell/components/PercentageBar';
|
||||
import { HCI } from '../types';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterUpgradeProgressList',
|
||||
@ -25,13 +27,45 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
await this.$store.dispatch('harvester/findAll', { type: HCI.UPGRADE });
|
||||
},
|
||||
|
||||
data() {
|
||||
return { open: true };
|
||||
},
|
||||
|
||||
computed: {
|
||||
showResumeButton() {
|
||||
return this.title === 'Upgrading Node';
|
||||
},
|
||||
latestUpgradeCR() {
|
||||
return this.$store.getters['harvester/all'](HCI.UPGRADE).find( (U) => U.isLatestUpgrade);
|
||||
},
|
||||
resumeUpgradePausedNodeEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('resumeUpgradePausedNode');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleSwitch() {
|
||||
this.open = !this.open;
|
||||
},
|
||||
async resumeNodeUpgrade(nodeName) {
|
||||
if (!this.latestUpgradeCR || !nodeName) return;
|
||||
|
||||
try {
|
||||
const upgradePauseMapString = this.latestUpgradeCR.metadata.annotations[HCI_ANNOTATIONS.NODE_UPGRADE_PAUSE_MAP] || '{}';
|
||||
const upgradePauseMap = JSON.parse(upgradePauseMapString);
|
||||
|
||||
// update the upgrade CR annotation harvesterhci.io/node-upgrade-pause-map to unpause the node upgrade process
|
||||
upgradePauseMap[`${ nodeName }`] = 'unpause';
|
||||
this.latestUpgradeCR.setAnnotation(HCI_ANNOTATIONS.NODE_UPGRADE_PAUSE_MAP, JSON.stringify(upgradePauseMap));
|
||||
await this.latestUpgradeCR.save();
|
||||
} catch (e) {
|
||||
console.error(`unable to update harvester upgrade CR annotations: ${ this.latestUpgradeCR.id }.`, e); // eslint-disable-line no-console
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -63,12 +97,28 @@ export default {
|
||||
v-for="(item, i) in list"
|
||||
:key="i"
|
||||
>
|
||||
<p>
|
||||
{{ item.name }} <span
|
||||
class="status"
|
||||
:class="{ [item.state]: true }"
|
||||
>{{ item.state }}</span>
|
||||
</p>
|
||||
<div class="upgrade-node-header">
|
||||
<div class="upgrade-node-title">
|
||||
<p>
|
||||
{{ item.name }}
|
||||
</p>
|
||||
<span
|
||||
class="status"
|
||||
:class="{ [item.state]: true }"
|
||||
>
|
||||
{{ item.state }}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
v-if="showResumeButton && resumeUpgradePausedNodeEnabled && item.state === 'Node-upgrade paused'"
|
||||
type="button"
|
||||
class="btn bg-info btn-sm"
|
||||
data-testid="add-item"
|
||||
@click="resumeNodeUpgrade(item.name)"
|
||||
>
|
||||
{{ t('action.resume') }}
|
||||
</button>
|
||||
</div>
|
||||
<PercentageBar
|
||||
:model-value="item.percent"
|
||||
preferred-direction="MORE"
|
||||
@ -102,10 +152,21 @@ export default {
|
||||
}
|
||||
}
|
||||
.custom-content {
|
||||
margin-bottom: 14px;
|
||||
p {
|
||||
.upgrade-node-title {
|
||||
flex: 1 0 80%;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.upgrade-node-header {
|
||||
display:flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
margin-bottom: 14px;
|
||||
|
||||
.status {
|
||||
float: right;
|
||||
}
|
||||
@ -117,6 +178,8 @@ export default {
|
||||
}
|
||||
.warning {
|
||||
color: var(--error);
|
||||
margin-bottom: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,7 +122,7 @@ export default {
|
||||
return this.$store.getters['currentCluster'].isLocal;
|
||||
},
|
||||
canEditClusterMembers() {
|
||||
return this.normanClusterRTBSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
|
||||
return this.schema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -0,0 +1,196 @@
|
||||
<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>
|
||||
123
pkg/harvester/components/settings/cluster-registration-url.vue
Normal file
123
pkg/harvester/components/settings/cluster-registration-url.vue
Normal file
@ -0,0 +1,123 @@
|
||||
<script>
|
||||
import MessageLink from '@shell/components/MessageLink';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import { HCI_SETTING } from '../../config/settings';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import { Banner } from '@components/Banner';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEditClusterRegistrationURL',
|
||||
|
||||
components: {
|
||||
LabeledInput, MessageLink, Checkbox, Banner
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
data() {
|
||||
let parseDefaultValue = {};
|
||||
|
||||
try {
|
||||
parseDefaultValue = JSON.parse(this.value.value);
|
||||
} catch (error) {
|
||||
parseDefaultValue.url = this.value.value;
|
||||
parseDefaultValue.insecureSkipTLSVerify = true;
|
||||
}
|
||||
|
||||
return {
|
||||
parseDefaultValue,
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
toCA() {
|
||||
return `${ HCI_SETTING.ADDITIONAL_CA }?mode=edit`;
|
||||
},
|
||||
clusterRegistrationTLSVerifyEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('clusterRegistrationTLSVerify');
|
||||
},
|
||||
registrationURL: {
|
||||
get() {
|
||||
return this.clusterRegistrationTLSVerifyEnabled ? this.parseDefaultValue.url : this.parseDefaultValue;
|
||||
},
|
||||
|
||||
set(value) {
|
||||
if (this.clusterRegistrationTLSVerifyEnabled) {
|
||||
this.parseDefaultValue.url = value;
|
||||
} else {
|
||||
this.parseDefaultValue = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getDefaultValue() {
|
||||
if (this.clusterRegistrationTLSVerifyEnabled) {
|
||||
return { url: '', insecureSkipTLSVerify: false };
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
updateUrl() {
|
||||
this.update();
|
||||
},
|
||||
|
||||
update() {
|
||||
if (this.clusterRegistrationTLSVerifyEnabled) {
|
||||
this.value['value'] = JSON.stringify(this.parseDefaultValue);
|
||||
} else {
|
||||
this.value['value'] = this.parseDefaultValue;
|
||||
}
|
||||
},
|
||||
|
||||
useDefault() {
|
||||
this.parseDefaultValue = this.getDefaultValue();
|
||||
},
|
||||
|
||||
updateInsecureSkipTLSVerify(newValue) {
|
||||
const { url = '' } = this.parseDefaultValue;
|
||||
|
||||
this.parseDefaultValue = { url, insecureSkipTLSVerify: newValue };
|
||||
this.update();
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="row"
|
||||
>
|
||||
<div class="col span-12">
|
||||
<Banner color="info">
|
||||
<MessageLink
|
||||
:to="toCA"
|
||||
target="_blank"
|
||||
prefix-label="harvester.setting.clusterRegistrationUrl.tip.prefix"
|
||||
middle-label="harvester.setting.clusterRegistrationUrl.tip.middle"
|
||||
suffix-label="harvester.setting.clusterRegistrationUrl.tip.suffix"
|
||||
/>
|
||||
</Banner>
|
||||
<LabeledInput
|
||||
v-model:value="registrationURL"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
:label="t('harvester.setting.clusterRegistrationUrl.url')"
|
||||
@update:value="updateUrl"
|
||||
/>
|
||||
<div v-if="clusterRegistrationTLSVerifyEnabled">
|
||||
<Checkbox
|
||||
v-model:value="parseDefaultValue.insecureSkipTLSVerify"
|
||||
class="check mb-5"
|
||||
type="checkbox"
|
||||
:label="t('harvester.setting.clusterRegistrationUrl.insecureSkipTLSVerify')"
|
||||
@update:value="updateInsecureSkipTLSVerify"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
104
pkg/harvester/components/settings/instance-manager-resources.vue
Normal file
104
pkg/harvester/components/settings/instance-manager-resources.vue
Normal file
@ -0,0 +1,104 @@
|
||||
<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>
|
||||
371
pkg/harvester/components/settings/rwx-network.vue
Normal file
371
pkg/harvester/components/settings/rwx-network.vue
Normal file
@ -0,0 +1,371 @@
|
||||
<script>
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import ArrayList from '@shell/components/form/ArrayList';
|
||||
import { isValidCIDR } from '@shell/utils/validators/cidr';
|
||||
import { _EDIT } from '@shell/config/query-params';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { HCI } from '../../types';
|
||||
import { NETWORK_TYPE } from '../../config/types';
|
||||
|
||||
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
|
||||
const SHARE_STORAGE_NETWORK = 'share-storage-network';
|
||||
const NETWORK = 'network';
|
||||
|
||||
const DEFAULT_DEDICATED_NETWORK = {
|
||||
vlan: '',
|
||||
clusterNetwork: '',
|
||||
range: '',
|
||||
exclude: [],
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'RwxNetworkSetting',
|
||||
|
||||
components: {
|
||||
RadioGroup,
|
||||
Banner,
|
||||
ArrayList,
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
},
|
||||
|
||||
props: {
|
||||
registerBeforeHook: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
|
||||
mode: {
|
||||
type: String,
|
||||
default: _EDIT,
|
||||
},
|
||||
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
await allHash({
|
||||
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
|
||||
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
|
||||
});
|
||||
},
|
||||
|
||||
data() {
|
||||
let enabled = false; // enabled / disabled options
|
||||
let shareStorageNetwork = false; // shareStorageNetwork / dedicatedRwxNetwork options
|
||||
let dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
||||
let networkType = L2VLAN;
|
||||
let exclude = [];
|
||||
|
||||
try {
|
||||
const parsedValue = JSON.parse(this.value.value || this.value.default || '{}');
|
||||
const parsedNetwork = parsedValue?.[NETWORK] || parsedValue || {};
|
||||
|
||||
if (parsedValue && typeof parsedValue === 'object') {
|
||||
shareStorageNetwork = !!parsedValue[SHARE_STORAGE_NETWORK];
|
||||
networkType = 'vlan' in parsedNetwork ? L2VLAN : UNTAGGED;
|
||||
dedicatedNetwork = {
|
||||
vlan: parsedNetwork.vlan || '',
|
||||
clusterNetwork: parsedNetwork.clusterNetwork || '',
|
||||
range: parsedNetwork.range || '',
|
||||
};
|
||||
exclude = parsedNetwork?.exclude?.toString().split(',') || [];
|
||||
enabled = shareStorageNetwork || !!(parsedNetwork.vlan || parsedNetwork.clusterNetwork || parsedNetwork.range);
|
||||
}
|
||||
} catch (error) {
|
||||
enabled = false;
|
||||
shareStorageNetwork = false;
|
||||
dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
||||
}
|
||||
|
||||
return {
|
||||
enabled,
|
||||
shareStorageNetwork,
|
||||
dedicatedNetwork,
|
||||
networkType,
|
||||
exclude,
|
||||
defaultAddValue: '',
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.registerBeforeHook) {
|
||||
this.registerBeforeHook(this.willSave, 'willSave');
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
showDedicatedNetworkConfig() {
|
||||
return this.enabled && !this.shareStorageNetwork;
|
||||
},
|
||||
|
||||
showVlan() {
|
||||
return this.networkType === L2VLAN;
|
||||
},
|
||||
|
||||
networkTypes() {
|
||||
return [L2VLAN, UNTAGGED];
|
||||
},
|
||||
|
||||
clusterNetworkOptions() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||
const clusterNetworksOptions = this.networkType === UNTAGGED ? clusterNetworks.filter((network) => network.id !== 'mgmt') : clusterNetworks;
|
||||
|
||||
return clusterNetworksOptions.map((network) => {
|
||||
const disabled = !network.isReadyForStorageNetwork;
|
||||
|
||||
return {
|
||||
label: disabled ? `${ network.id } (${ this.t('generic.notReady') })` : network.id,
|
||||
value: network.id,
|
||||
disabled,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onUpdateEnabled() {
|
||||
if (!this.enabled) {
|
||||
this.shareStorageNetwork = false;
|
||||
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
||||
}
|
||||
|
||||
this.update();
|
||||
},
|
||||
|
||||
onUpdateNetworkType() {
|
||||
if (this.shareStorageNetwork) {
|
||||
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
||||
}
|
||||
|
||||
this.update();
|
||||
},
|
||||
|
||||
onUpdateDedicatedType(neu) {
|
||||
this.dedicatedNetwork.clusterNetwork = '';
|
||||
|
||||
if (neu === L2VLAN) {
|
||||
this.dedicatedNetwork.vlan = '';
|
||||
} else {
|
||||
delete this.dedicatedNetwork.vlan;
|
||||
}
|
||||
|
||||
this.update();
|
||||
},
|
||||
|
||||
inputVlan(neu) {
|
||||
if (neu === '') {
|
||||
this.dedicatedNetwork.vlan = '';
|
||||
this.update();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = Number(neu);
|
||||
|
||||
if (newValue > 4094) {
|
||||
this.dedicatedNetwork.vlan = 4094;
|
||||
} else if (newValue < 1) {
|
||||
this.dedicatedNetwork.vlan = 1;
|
||||
} else {
|
||||
this.dedicatedNetwork.vlan = newValue;
|
||||
}
|
||||
|
||||
this.update();
|
||||
},
|
||||
|
||||
useDefault() {
|
||||
this.enabled = false;
|
||||
this.shareStorageNetwork = false;
|
||||
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
||||
this.update();
|
||||
},
|
||||
|
||||
update() {
|
||||
const value = { [SHARE_STORAGE_NETWORK]: false };
|
||||
|
||||
if (this.enabled && this.shareStorageNetwork) {
|
||||
value[SHARE_STORAGE_NETWORK] = true;
|
||||
}
|
||||
|
||||
if (this.showDedicatedNetworkConfig) {
|
||||
value[NETWORK] = {};
|
||||
|
||||
if (this.showVlan) {
|
||||
value[NETWORK].vlan = this.dedicatedNetwork.vlan;
|
||||
}
|
||||
|
||||
value[NETWORK].clusterNetwork = this.dedicatedNetwork.clusterNetwork;
|
||||
value[NETWORK].range = this.dedicatedNetwork.range;
|
||||
|
||||
const excludeList = this.exclude.filter((ip) => ip);
|
||||
|
||||
if (Array.isArray(excludeList) && excludeList.length > 0) {
|
||||
value[NETWORK].exclude = excludeList;
|
||||
}
|
||||
}
|
||||
|
||||
this.value.value = JSON.stringify(value);
|
||||
},
|
||||
|
||||
willSave() {
|
||||
this.update();
|
||||
|
||||
if (!this.showDedicatedNetworkConfig) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const errors = [];
|
||||
|
||||
if (this.showVlan && !this.dedicatedNetwork.vlan) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
|
||||
}
|
||||
|
||||
if (!this.dedicatedNetwork.clusterNetwork) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.clusterNetwork') }, true));
|
||||
}
|
||||
|
||||
if (!this.dedicatedNetwork.range) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.range.label') }, true));
|
||||
} else if (!isValidCIDR(this.dedicatedNetwork.range)) {
|
||||
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true));
|
||||
}
|
||||
|
||||
if (this.exclude) {
|
||||
const hasInvalidCIDR = this.exclude.find((cidr) => {
|
||||
return cidr && !isValidCIDR(cidr);
|
||||
});
|
||||
|
||||
if (hasInvalidCIDR) {
|
||||
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', null, true));
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Promise.reject(errors);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="mode">
|
||||
<Banner color="warning">
|
||||
<t
|
||||
k="harvester.setting.rwxNetwork.warning"
|
||||
:raw="true"
|
||||
/>
|
||||
</Banner>
|
||||
<RadioGroup
|
||||
v-model:value="enabled"
|
||||
class="mb-20"
|
||||
name="rwx-network-enable"
|
||||
:options="[true,false]"
|
||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||
@update:value="onUpdateEnabled"
|
||||
/>
|
||||
|
||||
<RadioGroup
|
||||
v-if="enabled"
|
||||
v-model:value="shareStorageNetwork"
|
||||
class="mb-20"
|
||||
name="rwx-network-type"
|
||||
:options="[true,false]"
|
||||
:labels="[t('harvester.setting.rwxNetwork.shareStorageNetwork'), t('harvester.setting.rwxNetwork.dedicatedRwxNetwork')]"
|
||||
@update:value="onUpdateNetworkType"
|
||||
/>
|
||||
<Banner
|
||||
v-if="shareStorageNetwork"
|
||||
class="mb-20"
|
||||
color="warning"
|
||||
>
|
||||
<t
|
||||
k="harvester.setting.rwxNetwork.shareStorageNetworkWarning"
|
||||
:raw="true"
|
||||
/>
|
||||
</Banner>
|
||||
<template v-if="showDedicatedNetworkConfig">
|
||||
<LabeledSelect
|
||||
v-model:value="networkType"
|
||||
class="mb-20"
|
||||
:options="networkTypes"
|
||||
:mode="mode"
|
||||
:label="t('harvester.fields.type')"
|
||||
required
|
||||
@update:value="onUpdateDedicatedType"
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-if="showVlan"
|
||||
v-model:value.number="dedicatedNetwork.vlan"
|
||||
type="number"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
required
|
||||
placeholder="e.g. 1 - 4094"
|
||||
label-key="harvester.setting.storageNetwork.vlan"
|
||||
@update:value="inputVlan"
|
||||
/>
|
||||
|
||||
<LabeledSelect
|
||||
v-model:value="dedicatedNetwork.clusterNetwork"
|
||||
label-key="harvester.setting.storageNetwork.clusterNetwork"
|
||||
class="mb-20"
|
||||
required
|
||||
:options="clusterNetworkOptions"
|
||||
@update:value="update"
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-model:value="dedicatedNetwork.range"
|
||||
class="mb-5"
|
||||
:mode="mode"
|
||||
required
|
||||
:placeholder="t('harvester.setting.storageNetwork.range.placeholder')"
|
||||
label-key="harvester.setting.storageNetwork.range.label"
|
||||
@update:value="update"
|
||||
/>
|
||||
|
||||
<ArrayList
|
||||
v-model:value="exclude"
|
||||
:show-header="true"
|
||||
:default-add-value="defaultAddValue"
|
||||
:mode="mode"
|
||||
:add-label="t('harvester.setting.storageNetwork.exclude.addIp')"
|
||||
class="mt-20"
|
||||
@update:value="update"
|
||||
>
|
||||
<template #column-headers>
|
||||
<div class="box mb-10">
|
||||
<div class="key">
|
||||
{{ t('harvester.setting.storageNetwork.exclude.label') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #columns="scope">
|
||||
<div class="key">
|
||||
<input
|
||||
v-model="scope.row.value"
|
||||
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
||||
@update:value="update"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArrayList>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@ -18,16 +18,20 @@ export default {
|
||||
|
||||
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
||||
|
||||
try {
|
||||
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
||||
const response = await this.$store.dispatch('harvester/request', { url });
|
||||
if (this.customSupportBundleFeatureEnabled) {
|
||||
try {
|
||||
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
||||
const response = await this.$store.dispatch('harvester/request', { url });
|
||||
|
||||
this.defaultNamespaces = response.data || [];
|
||||
} catch (error) {
|
||||
this.defaultNamespaces = response.data || [];
|
||||
} catch (error) {
|
||||
this.defaultNamespaces = [];
|
||||
}
|
||||
} else {
|
||||
this.defaultNamespaces = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
data() {
|
||||
@ -42,24 +46,38 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
customSupportBundleFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
|
||||
},
|
||||
|
||||
allNamespaces() {
|
||||
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
||||
},
|
||||
|
||||
filteredNamespaces() {
|
||||
if (!this.customSupportBundleFeatureEnabled) {
|
||||
return this.allNamespaces;
|
||||
}
|
||||
|
||||
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
|
||||
|
||||
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
|
||||
},
|
||||
|
||||
namespaceOptions() {
|
||||
const mappedNamespaces = this.filteredNamespaces.map((ns) => ({ label: ns, value: ns }));
|
||||
|
||||
if (!this.customSupportBundleFeatureEnabled) {
|
||||
return mappedNamespaces;
|
||||
}
|
||||
|
||||
const allSelected =
|
||||
this.namespaces.length === this.filteredNamespaces.length &&
|
||||
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
|
||||
|
||||
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
|
||||
|
||||
return [controlOption, ...this.filteredNamespaces];
|
||||
return [controlOption, ...mappedNamespaces];
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -4,6 +4,8 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { NODE } from '@shell/config/types';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterUpgradeConfig',
|
||||
@ -15,6 +17,13 @@ export default {
|
||||
},
|
||||
mixins: [CreateEditView],
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const hash = { nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }) };
|
||||
|
||||
await allHash(hash);
|
||||
},
|
||||
|
||||
data() {
|
||||
let parseDefaultValue = {};
|
||||
|
||||
@ -39,7 +48,25 @@ export default {
|
||||
{ value: 'skip', label: 'skip' },
|
||||
{ value: 'parallel', label: 'parallel' }
|
||||
];
|
||||
}
|
||||
},
|
||||
nodeUpgradeOptions() {
|
||||
return [
|
||||
{ value: 'auto', label: 'auto' },
|
||||
{ value: 'manual', label: 'manual' }
|
||||
];
|
||||
},
|
||||
nodesOptions() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const nodes = this.$store.getters[`${ inStore }/all`](NODE);
|
||||
|
||||
return nodes.map((node) => ({ value: node.id, label: node.name }));
|
||||
},
|
||||
showPauseNodes() {
|
||||
return this.parseDefaultValue.nodeUpgradeOption?.strategy?.mode === 'manual';
|
||||
},
|
||||
resumeUpgradePausedNodeEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('resumeUpgradePausedNode');
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
@ -48,6 +75,18 @@ export default {
|
||||
|
||||
methods: {
|
||||
normalizeValue(obj) {
|
||||
// handle nodeUpgradeOption.strategy
|
||||
if (obj?.nodeUpgradeOption?.strategy?.mode === 'auto') {
|
||||
delete obj.nodeUpgradeOption.strategy.pauseNodes;
|
||||
}
|
||||
|
||||
if (obj?.nodeUpgradeOption?.strategy?.mode === 'manual') {
|
||||
if (!Array.isArray(obj.nodeUpgradeOption.strategy.pauseNodes)) {
|
||||
obj.nodeUpgradeOption.strategy.pauseNodes = this.nodesOptions.map((node) => node.value);
|
||||
}
|
||||
}
|
||||
|
||||
// handle imagePreloadOption.strategy
|
||||
if (!obj.imagePreloadOption) {
|
||||
obj.imagePreloadOption = { strategy: { type: 'sequential' } };
|
||||
}
|
||||
@ -105,8 +144,8 @@ export default {
|
||||
this.update();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -144,6 +183,28 @@ export default {
|
||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||
@update:value="update"
|
||||
/>
|
||||
<div v-if="resumeUpgradePausedNodeEnabled">
|
||||
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.nodeUpgradeOption') }}</b></label>
|
||||
<LabeledSelect
|
||||
v-model:value="parseDefaultValue.nodeUpgradeOption.strategy.mode"
|
||||
class="mb-20 label-select"
|
||||
:mode="mode"
|
||||
:label="t('harvester.setting.upgrade.strategy')"
|
||||
:options="nodeUpgradeOptions"
|
||||
@update:value="update"
|
||||
/>
|
||||
<LabeledSelect
|
||||
v-if="showPauseNodes"
|
||||
v-model:value="parseDefaultValue.nodeUpgradeOption.strategy.pauseNodes"
|
||||
class="mb-20 label-select"
|
||||
:clearable="true"
|
||||
:multiple="true"
|
||||
:mode="mode"
|
||||
:label="t('harvester.setting.upgrade.pauseNodes')"
|
||||
:options="nodesOptions"
|
||||
@update:value="update"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="errors.length"
|
||||
class="error"
|
||||
|
||||
@ -69,6 +69,15 @@ export default {
|
||||
:mode="mode"
|
||||
label-key="harvester.setting.vmForceDeletionPolicy.period"
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-if="parseDefaultValue.enable"
|
||||
v-model:value.number="parseDefaultValue.vmMigrationTimeout"
|
||||
class="mb-20"
|
||||
type="number"
|
||||
:mode="mode"
|
||||
label-key="harvester.setting.vmForceDeletionPolicy.vmMigrationTimeout"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -9,4 +9,5 @@ export const DOC = {
|
||||
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
|
||||
VPC_CONFIGURATION_EXAMPLES: `/networking/kubeovn-vpc#vpc-peering-configuration-examples`,
|
||||
NETWORK_POLICY: `/networking/kubeovn-vm-isolation/#network-policies`,
|
||||
TRANSPARENT_HUGEPAGES: `https://docs.kernel.org/admin-guide/mm/transhuge.html`,
|
||||
};
|
||||
|
||||
@ -53,8 +53,23 @@ const FEATURE_FLAGS = {
|
||||
'vmMachineTypeAuto',
|
||||
'lhV2VolExpansion',
|
||||
'l2VlanTrunkMode',
|
||||
'kubevirtMigration'
|
||||
]
|
||||
'kubevirtMigration',
|
||||
'hotplugNic',
|
||||
'resumeUpgradePausedNode',
|
||||
],
|
||||
'v1.7.1': [],
|
||||
'v1.8.0': [
|
||||
'hotplugCdRom',
|
||||
'supportBundleFileNameSetting',
|
||||
'clusterRegistrationTLSVerify',
|
||||
'vGPUAsPCIDevice',
|
||||
'instanceManagerResourcesSetting',
|
||||
'rwxNetworkSetting',
|
||||
'createPVCWithDataVolume',
|
||||
'clusterPodSecurityStandardSetting'
|
||||
],
|
||||
'v1.8.1': [],
|
||||
'v1.9.0': [],
|
||||
};
|
||||
|
||||
const generateFeatureFlags = () => {
|
||||
|
||||
@ -35,8 +35,21 @@ import {
|
||||
SNAPSHOT_TARGET_VOLUME,
|
||||
IMAGE_VIRTUAL_SIZE,
|
||||
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';
|
||||
import { ADD_ONS } from './harvester-map';
|
||||
import { registerAddonSideNav } from '../utils/dynamic-nav';
|
||||
|
||||
const TEMPLATE = HCI.VM_VERSION;
|
||||
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
|
||||
@ -68,7 +81,6 @@ export function init($plugin, store) {
|
||||
configureType,
|
||||
virtualType,
|
||||
weightGroup,
|
||||
weightType,
|
||||
} = $plugin.DSL(store, PRODUCT_NAME);
|
||||
|
||||
const isSingleVirtualCluster = process.env.rancherEnv === PRODUCT_NAME;
|
||||
@ -155,7 +167,7 @@ export function init($plugin, store) {
|
||||
group: 'Root',
|
||||
name: HCI.HOST,
|
||||
namespaced: true,
|
||||
weight: 399,
|
||||
weight: 499,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.HOST }
|
||||
@ -187,7 +199,7 @@ export function init($plugin, store) {
|
||||
group: 'root',
|
||||
name: HCI.VM,
|
||||
namespaced: true,
|
||||
weight: 299,
|
||||
weight: 498,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.VM }
|
||||
@ -195,6 +207,142 @@ export function init($plugin, store) {
|
||||
exact: false
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// VM Import Controller UI Flow
|
||||
// ===========================================================================
|
||||
// Define group (Hidden by default)
|
||||
weightGroup('vmimport', 0, false);
|
||||
|
||||
// VirtualMachineImport
|
||||
headers(HCI.VMIMPORT, [
|
||||
STATE,
|
||||
NAME_COL,
|
||||
NAMESPACE_COL,
|
||||
VM_IMPORT_SOURCE_VM,
|
||||
VM_IMPORT_SOURCE_CLUSTER,
|
||||
VM_IMPORT_STATUS,
|
||||
AGE
|
||||
]);
|
||||
configureType(HCI.VMIMPORT, {
|
||||
resource: HCI.VMIMPORT,
|
||||
resourceDetail: HCI.VMIMPORT,
|
||||
resourceEdit: HCI.VMIMPORT,
|
||||
location: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.VMIMPORT }
|
||||
}
|
||||
});
|
||||
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
|
||||
name: HCI.VMIMPORT,
|
||||
labelKey: 'harvester.addons.vmImport.labels.vmimport',
|
||||
group: 'vmimport',
|
||||
namespaced: true,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.VMIMPORT }
|
||||
}
|
||||
});
|
||||
|
||||
// Source: VMware
|
||||
headers(HCI.VMIMPORT_SOURCE_V, [
|
||||
STATE,
|
||||
NAME_COL,
|
||||
VM_IMPORT_SOURCE_V_ENDPOINT,
|
||||
VM_IMPORT_SOURCE_V_DC,
|
||||
VM_IMPORT_SOURCE_V_STATUS,
|
||||
AGE
|
||||
]);
|
||||
configureType(HCI.VMIMPORT_SOURCE_V, {
|
||||
resource: HCI.VMIMPORT_SOURCE_V,
|
||||
resourceDetail: HCI.VMIMPORT_SOURCE_V,
|
||||
resourceEdit: HCI.VMIMPORT_SOURCE_V,
|
||||
location: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.VMIMPORT_SOURCE_V }
|
||||
}
|
||||
});
|
||||
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
|
||||
name: HCI.VMIMPORT_SOURCE_V,
|
||||
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare',
|
||||
group: 'vmimport',
|
||||
namespaced: true,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.VMIMPORT_SOURCE_V }
|
||||
}
|
||||
});
|
||||
|
||||
// Source: OpenStack
|
||||
headers(HCI.VMIMPORT_SOURCE_O, [
|
||||
STATE,
|
||||
NAME_COL,
|
||||
VM_IMPORT_SOURCE_O_ENDPOINT,
|
||||
VM_IMPORT_SOURCE_O_REGION,
|
||||
VM_IMPORT_SOURCE_O_STATUS,
|
||||
AGE
|
||||
]);
|
||||
configureType(HCI.VMIMPORT_SOURCE_O, {
|
||||
resource: HCI.VMIMPORT_SOURCE_O,
|
||||
resourceDetail: HCI.VMIMPORT_SOURCE_O,
|
||||
resourceEdit: HCI.VMIMPORT_SOURCE_O,
|
||||
location: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.VMIMPORT_SOURCE_O }
|
||||
}
|
||||
});
|
||||
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
|
||||
name: HCI.VMIMPORT_SOURCE_O,
|
||||
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack',
|
||||
group: 'vmimport',
|
||||
namespaced: true,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.VMIMPORT_SOURCE_O }
|
||||
}
|
||||
});
|
||||
|
||||
// Source: OVA
|
||||
headers(HCI.VMIMPORT_SOURCE_OVA, [
|
||||
STATE,
|
||||
NAME_COL,
|
||||
VM_IMPORT_SOURCE_OVA_URL,
|
||||
VM_IMPORT_SOURCE_OVA_STATUS,
|
||||
AGE
|
||||
]);
|
||||
configureType(HCI.VMIMPORT_SOURCE_OVA, {
|
||||
resource: HCI.VMIMPORT_SOURCE_OVA,
|
||||
resourceDetail: HCI.VMIMPORT_SOURCE_OVA,
|
||||
resourceEdit: HCI.VMIMPORT_SOURCE_OVA,
|
||||
location: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.VMIMPORT_SOURCE_OVA }
|
||||
}
|
||||
});
|
||||
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
|
||||
name: HCI.VMIMPORT_SOURCE_OVA,
|
||||
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA',
|
||||
group: 'vmimport',
|
||||
namespaced: true,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.VMIMPORT_SOURCE_OVA }
|
||||
}
|
||||
});
|
||||
|
||||
// Enable SideNav based on Addon Status
|
||||
registerAddonSideNav(store, PRODUCT_NAME, {
|
||||
addonName: ADD_ONS.VM_IMPORT_CONTROLLER,
|
||||
resourceType: HCI.ADD_ONS,
|
||||
navGroup: 'vmimport',
|
||||
types: [
|
||||
HCI.VMIMPORT_SOURCE_V,
|
||||
HCI.VMIMPORT_SOURCE_O,
|
||||
HCI.VMIMPORT_SOURCE_OVA,
|
||||
HCI.VMIMPORT
|
||||
]
|
||||
});
|
||||
// ===========================================================================
|
||||
|
||||
basicType([HCI.VOLUME]);
|
||||
configureType(HCI.VOLUME, {
|
||||
location: {
|
||||
@ -212,7 +360,7 @@ export function init($plugin, store) {
|
||||
ifHaveType: PVC,
|
||||
name: HCI.VOLUME,
|
||||
namespaced: true,
|
||||
weight: 199,
|
||||
weight: 497,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.VOLUME }
|
||||
@ -238,7 +386,7 @@ export function init($plugin, store) {
|
||||
group: 'root',
|
||||
name: HCI.IMAGE,
|
||||
namespaced: true,
|
||||
weight: 198,
|
||||
weight: 496,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.IMAGE }
|
||||
@ -253,7 +401,7 @@ export function init($plugin, store) {
|
||||
group: 'root',
|
||||
namespaced: true,
|
||||
name: 'projects-namespaces',
|
||||
weight: 98,
|
||||
weight: 495,
|
||||
route: { name: `${ PRODUCT_NAME }-c-cluster-projectsnamespaces` },
|
||||
exact: true,
|
||||
});
|
||||
@ -265,7 +413,7 @@ export function init($plugin, store) {
|
||||
labelKey: 'harvester.namespace.label',
|
||||
name: NAMESPACE,
|
||||
namespaced: true,
|
||||
weight: 89,
|
||||
weight: 495,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: NAMESPACE }
|
||||
@ -443,9 +591,8 @@ export function init($plugin, store) {
|
||||
'backupAndSnapshot'
|
||||
);
|
||||
|
||||
weightGroup('networks', 300, true);
|
||||
weightType(NAMESPACE, 299, true);
|
||||
weightGroup('backupAndSnapshot', 289, true);
|
||||
weightGroup('networks', 494, true);
|
||||
weightGroup('backupAndSnapshot', 493, true);
|
||||
|
||||
basicType(
|
||||
[
|
||||
@ -539,7 +686,7 @@ export function init($plugin, store) {
|
||||
},
|
||||
resource: NETWORK_ATTACHMENT,
|
||||
resourceDetail: HCI.NETWORK_ATTACHMENT,
|
||||
resourceEdit: HCI.NETWORK_ATTACHMENT
|
||||
resourceEdit: HCI.NETWORK_ATTACHMENT,
|
||||
});
|
||||
|
||||
virtualType({
|
||||
|
||||
@ -39,6 +39,12 @@ export const VOLUME_TYPE = [{
|
||||
value: 'cd-rom'
|
||||
}];
|
||||
|
||||
export const VOLUME_HOTPLUG_ACTION = {
|
||||
INSERT_CDROM_IMAGE: 'INSERT_CDROM_IMAGE',
|
||||
EJECT_CDROM_IMAGE: 'EJECT_CDROM_IMAGE',
|
||||
DETACH_DISK: 'DETACH_DISK'
|
||||
};
|
||||
|
||||
export const ACCESS_CREDENTIALS = {
|
||||
RESET_PWD: 'userPassword',
|
||||
INJECT_SSH: 'sshPublicKey'
|
||||
|
||||
@ -28,6 +28,7 @@ export const HCI = {
|
||||
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
|
||||
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
|
||||
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
|
||||
CLONE_BACKEND_STORAGE_STATUS: 'harvesterhci.io/clone-backend-storage-status',
|
||||
MIGRATION_STATE: 'harvesterhci.io/migrationState',
|
||||
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
|
||||
IMAGE_NAME: 'harvesterhci.io/image-name',
|
||||
@ -50,6 +51,7 @@ export const HCI = {
|
||||
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
|
||||
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
|
||||
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
|
||||
ADDON_DISPLAYNAME: 'addon.harvesterhci.io/displayName',
|
||||
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
|
||||
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
|
||||
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
|
||||
@ -76,4 +78,7 @@ export const HCI = {
|
||||
CLONE_STRATEGY: 'cdi.harvesterhci.io/storageProfileCloneStrategy',
|
||||
VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes',
|
||||
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
|
||||
MAC_ADDRESS: 'harvesterhci.io/mac-address',
|
||||
NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map',
|
||||
CDI_POPULATOR_KIND: 'cdi.kubevirt.io/storage.populator.kind',
|
||||
};
|
||||
|
||||
@ -16,9 +16,11 @@ export const HCI_SETTING = {
|
||||
DEFAULT_STORAGE_CLASS: 'default-storage-class',
|
||||
SUPPORT_BUNDLE_TIMEOUT: 'support-bundle-timeout',
|
||||
SUPPORT_BUNDLE_EXPIRATION: 'support-bundle-expiration',
|
||||
SUPPORT_BUNDLE_FILE_NAME: 'support-bundle-file-name',
|
||||
SUPPORT_BUNDLE_IMAGE: 'support-bundle-image',
|
||||
SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT: 'support-bundle-node-collection-timeout',
|
||||
STORAGE_NETWORK: 'storage-network',
|
||||
RWX_NETWORK: 'rwx-network',
|
||||
VM_FORCE_RESET_POLICY: 'vm-force-reset-policy',
|
||||
SSL_CERTIFICATES: 'ssl-certificates',
|
||||
SSL_PARAMETERS: 'ssl-parameters',
|
||||
@ -38,7 +40,9 @@ export const HCI_SETTING = {
|
||||
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
||||
RANCHER_CLUSTER: 'rancher-cluster',
|
||||
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 = {
|
||||
@ -71,11 +75,17 @@ export const HCI_ALLOWED_SETTINGS = {
|
||||
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: { kind: 'number' },
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: { kind: 'number' },
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_FILE_NAME]: {
|
||||
kind: 'string', canReset: true, featureFlag: 'supportBundleFileNameSetting'
|
||||
},
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { kind: 'number', featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.STORAGE_NETWORK]: {
|
||||
kind: 'custom', from: 'import', canReset: true
|
||||
},
|
||||
[HCI_SETTING.RWX_NETWORK]: {
|
||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'rwxNetworkSetting'
|
||||
},
|
||||
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.SSL_PARAMETERS]: {
|
||||
@ -118,12 +128,19 @@ export const HCI_ALLOWED_SETTINGS = {
|
||||
},
|
||||
[HCI_SETTING.KUBEVIRT_MIGRATION]: {
|
||||
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'
|
||||
}
|
||||
};
|
||||
|
||||
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
||||
[HCI_SETTING.CLUSTER_REGISTRATION_URL]: {
|
||||
kind: 'url',
|
||||
kind: 'custom',
|
||||
from: 'import',
|
||||
canReset: true,
|
||||
},
|
||||
[HCI_SETTING.UI_PL]: {
|
||||
|
||||
@ -131,3 +131,102 @@ export const PROVIDER = {
|
||||
value: 'spec.provider',
|
||||
align: 'left',
|
||||
};
|
||||
|
||||
// Source VM column in migration.harvesterhci.io.virtualmachineimport list page
|
||||
export const VM_IMPORT_SOURCE_VM = {
|
||||
name: 'sourceVm',
|
||||
labelKey: 'harvester.tableHeaders.vmImportSourceVm',
|
||||
value: 'spec.virtualMachineName',
|
||||
sort: 'spec.virtualMachineName',
|
||||
align: 'left',
|
||||
};
|
||||
|
||||
// Source Cluster column in migration.harvesterhci.io.virtualmachineimport list page
|
||||
export const VM_IMPORT_SOURCE_CLUSTER = {
|
||||
name: 'sourceCluster',
|
||||
labelKey: 'harvester.tableHeaders.vmImportSourceCluster',
|
||||
value: 'spec.sourceCluster.name',
|
||||
sort: 'spec.sourceCluster.name',
|
||||
align: 'left',
|
||||
};
|
||||
|
||||
// Import Status column in migration.harvesterhci.io.virtualmachineimport list page
|
||||
export const VM_IMPORT_STATUS = {
|
||||
name: 'importStatus',
|
||||
labelKey: 'harvester.tableHeaders.vmImportStatus',
|
||||
value: 'status.importStatus',
|
||||
sort: 'status.importStatus',
|
||||
align: 'left',
|
||||
};
|
||||
|
||||
// Datacenter column in migration.harvesterhci.io.vmwaresource list page
|
||||
export const VM_IMPORT_SOURCE_V_DC = {
|
||||
name: 'datacenter',
|
||||
labelKey: 'harvester.tableHeaders.vmImportSourceVDatacenter',
|
||||
value: 'spec.dc',
|
||||
sort: 'spec.dc',
|
||||
align: 'left',
|
||||
};
|
||||
|
||||
// Endpoint column in migration.harvesterhci.io.vmwaresource list page
|
||||
export const VM_IMPORT_SOURCE_V_ENDPOINT = {
|
||||
name: 'endpoint',
|
||||
labelKey: 'harvester.tableHeaders.vmImportSourceVEndpoint',
|
||||
value: 'spec.endpoint',
|
||||
sort: 'spec.endpoint',
|
||||
align: 'left',
|
||||
};
|
||||
|
||||
// Cluster Status column in migration.harvesterhci.io.vmwaresource list page
|
||||
export const VM_IMPORT_SOURCE_V_STATUS = {
|
||||
name: 'clusterStatus',
|
||||
labelKey: 'harvester.tableHeaders.vmImportSourceVClusterStatus',
|
||||
value: 'status.status',
|
||||
sort: 'status.status',
|
||||
align: 'left',
|
||||
};
|
||||
|
||||
// Region column in migration.harvesterhci.io.openstacksource list page
|
||||
export const VM_IMPORT_SOURCE_O_REGION = {
|
||||
name: 'region',
|
||||
labelKey: 'harvester.tableHeaders.vmImportSourceORegion',
|
||||
value: 'spec.region',
|
||||
sort: 'spec.region',
|
||||
align: 'left',
|
||||
};
|
||||
|
||||
// Endpoint column in migration.harvesterhci.io.openstacksource list page
|
||||
export const VM_IMPORT_SOURCE_O_ENDPOINT = {
|
||||
name: 'endpoint',
|
||||
labelKey: 'harvester.tableHeaders.vmImportSourceOEndpoint',
|
||||
value: 'spec.endpoint',
|
||||
sort: 'spec.endpoint',
|
||||
align: 'left',
|
||||
};
|
||||
|
||||
// Cluster Status column in migration.harvesterhci.io.openstacksource list page
|
||||
export const VM_IMPORT_SOURCE_O_STATUS = {
|
||||
name: 'clusterStatus',
|
||||
labelKey: 'harvester.tableHeaders.vmImportSourceOClusterStatus',
|
||||
value: 'status.status',
|
||||
sort: 'status.status',
|
||||
align: 'left',
|
||||
};
|
||||
|
||||
// URL column in migration.harvesterhci.io.ovasource list page
|
||||
export const VM_IMPORT_SOURCE_OVA_URL = {
|
||||
name: 'url',
|
||||
labelKey: 'harvester.tableHeaders.vmImportSourceOVAUrl',
|
||||
value: 'spec.url',
|
||||
sort: 'spec.url',
|
||||
align: 'left',
|
||||
};
|
||||
|
||||
// Status column in migration.harvesterhci.io.ovasource list page
|
||||
export const VM_IMPORT_SOURCE_OVA_STATUS = {
|
||||
name: 'status',
|
||||
labelKey: 'harvester.tableHeaders.vmImportSourceOVAStatus',
|
||||
value: 'status.status',
|
||||
sort: 'status.status',
|
||||
align: 'left',
|
||||
};
|
||||
|
||||
@ -29,3 +29,20 @@ export const L2VLAN_MODE = {
|
||||
ACCESS: 'access',
|
||||
TRUNK: 'trunk',
|
||||
};
|
||||
|
||||
export const VMIMPORT_SOURCE_PROVIDER = {
|
||||
VMWARE: 'vmware',
|
||||
OPENSTACK: 'openstack',
|
||||
OVA: 'ova',
|
||||
};
|
||||
|
||||
export const VMIMPORT_SOURCE_KINDS = {
|
||||
VMWARE: 'VmwareSource',
|
||||
OPENSTACK: 'OpenstackSource',
|
||||
OVA: 'OvaSource',
|
||||
};
|
||||
|
||||
export const CDI_POPULATOR_KIND = {
|
||||
VOLUME_IMPORT_SOURCE: 'VolumeImportSource',
|
||||
VOLUME_CLONE_SOURCE: 'VolumeCloneSource',
|
||||
};
|
||||
|
||||
119
pkg/harvester/detail/harvesterhci.io.host/HarvesterHugepages.vue
Normal file
119
pkg/harvester/detail/harvesterhci.io.host/HarvesterHugepages.vue
Normal file
@ -0,0 +1,119 @@
|
||||
<script>
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import { HCI } from '../../types';
|
||||
import { DOC } from '../../config/doc-links';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterHugepages',
|
||||
components: { LabelValue },
|
||||
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
docsTransparentHugepagesLink() {
|
||||
return DOC.TRANSPARENT_HUGEPAGES;
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
|
||||
|
||||
this.hugepages = hash.find((node) => {
|
||||
return node.id === this.node.id;
|
||||
}) || {};
|
||||
},
|
||||
|
||||
data() {
|
||||
return { hugepages: {} };
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="hugepages.status">
|
||||
<h2>{{ t('harvester.host.hugepages.meminfo') }}</h2>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.hugepages.status.anon')"
|
||||
:value="hugepages.status.meminfo.anonHugePages"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.hugepages.status.size')"
|
||||
:value="hugepages.status.meminfo.hugepageSize"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.hugepages.status.total')"
|
||||
:value="hugepages.status.meminfo.hugePagesTotal"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.hugepages.status.free')"
|
||||
:value="hugepages.status.meminfo.hugePagesFree"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.hugepages.status.rsvd')"
|
||||
:value="hugepages.status.meminfo.hugePagesRsvd"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.hugepages.status.surp')"
|
||||
:value="hugepages.status.meminfo.hugePagesSurp"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<hr class="divider" />
|
||||
<h3>
|
||||
<t
|
||||
k="harvester.host.hugepages.transparent.title"
|
||||
:raw="true"
|
||||
:url="docsTransparentHugepagesLink"
|
||||
/>
|
||||
</h3>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-4">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.hugepages.transparent.enabled')"
|
||||
:value="hugepages.spec.transparent.enabled"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.hugepages.transparent.shmemEnabled')"
|
||||
:value="hugepages.spec.transparent.shmemEnabled"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.hugepages.transparent.defrag')"
|
||||
:value="hugepages.spec.transparent.defrag"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@ -27,6 +27,7 @@ import Instance from './VirtualMachineInstance';
|
||||
import Disk from './HarvesterHostDisk';
|
||||
import VlanStatus from './VlanStatus';
|
||||
import HarvesterKsmtuned from './HarvesterKsmtuned.vue';
|
||||
import HarvesterHugepages from './HarvesterHugepages.vue';
|
||||
import HarvesterSeeder from './HarvesterSeeder';
|
||||
|
||||
const LONGHORN_SYSTEM = 'longhorn-system';
|
||||
@ -46,6 +47,7 @@ export default {
|
||||
VlanStatus,
|
||||
LabelValue,
|
||||
HarvesterKsmtuned,
|
||||
HarvesterHugepages,
|
||||
Loading,
|
||||
SortableTable,
|
||||
HarvesterSeeder,
|
||||
@ -209,6 +211,12 @@ export default {
|
||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
||||
},
|
||||
|
||||
hasHugepagesSchema() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.HUGEPAGES);
|
||||
},
|
||||
|
||||
hasBlockDevicesSchema() {
|
||||
return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE);
|
||||
},
|
||||
@ -468,6 +476,16 @@ export default {
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
v-if="hasHugepagesSchema"
|
||||
name="hugepages"
|
||||
:weight="0"
|
||||
:show-header="false"
|
||||
:label="t('harvester.host.tabs.hugepages')"
|
||||
>
|
||||
<HarvesterHugepages :node="value" />
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
v-if="seederEnabled"
|
||||
name="seeder"
|
||||
|
||||
@ -75,7 +75,7 @@ export default {
|
||||
<div class="row">
|
||||
<div class="col span-6 mb-20">
|
||||
<LabelValue
|
||||
:name="t('harvester.schedule.cron')"
|
||||
:name="t('harvester.schedule.cron.label')"
|
||||
:value="cronExpression"
|
||||
/>
|
||||
</div>
|
||||
|
||||
1446
pkg/harvester/detail/kubeovn.io.vpc.vue
Normal file
1446
pkg/harvester/detail/kubeovn.io.vpc.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -213,6 +213,7 @@ export default {
|
||||
const diskRows = this.getDiskRows(neu);
|
||||
|
||||
this['diskRows'] = diskRows;
|
||||
this['networkRows'] = this.getNetworkRows(neu, { fromTemplate: false, init: false });
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
@ -265,6 +266,7 @@ export default {
|
||||
<Network
|
||||
v-model:value="networkRows"
|
||||
mode="view"
|
||||
:vm="value"
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@ export default {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
@ -43,7 +43,7 @@ export default {
|
||||
...mapState('action-menu', ['modalData']),
|
||||
|
||||
title() {
|
||||
return this.modalData.title || 'dialog.promptRemove.title';
|
||||
return this.modalData?.title || 'dialog.promptRemove.title';
|
||||
},
|
||||
|
||||
formattedType() {
|
||||
@ -51,7 +51,7 @@ export default {
|
||||
},
|
||||
|
||||
warningMessage() {
|
||||
if (this.modalData.warningMessage) return this.modalData.warningMessage;
|
||||
if (this.modalData?.warningMessage) return this.modalData.warningMessage;
|
||||
|
||||
const isPlural = this.type.endsWith('s');
|
||||
const thisOrThese = isPlural ? 'these' : 'this';
|
||||
@ -145,6 +145,7 @@ export default {
|
||||
try {
|
||||
for (const resource of this.resources) {
|
||||
await resource.remove();
|
||||
if (this.modalData?.extraActionAfterRemove) await this.modalData.extraActionAfterRemove();
|
||||
}
|
||||
buttonDone(true);
|
||||
this.close();
|
||||
|
||||
@ -4,6 +4,7 @@ import { Card } from '@components/Card';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { escapeHtml } from '@shell/utils/string';
|
||||
import { HCI } from '../types';
|
||||
import { getHarvesterUserName } from '../utils/auth';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEnablePciPassthrough',
|
||||
@ -34,16 +35,7 @@ export default {
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
// 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;
|
||||
}
|
||||
const userName = getHarvesterUserName(this.$store.getters);
|
||||
|
||||
for (let i = 0; i < this.resources.length; i++) {
|
||||
const actionResource = this.resources[i];
|
||||
|
||||
@ -4,6 +4,7 @@ import { Card } from '@components/Card';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { escapeHtml } from '@shell/utils/string';
|
||||
import { HCI } from '../types';
|
||||
import { getHarvesterUserName } from '../utils/auth';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEnableUSBPassthrough',
|
||||
@ -34,16 +35,7 @@ export default {
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
// 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;
|
||||
}
|
||||
const userName = getHarvesterUserName(this.$store.getters);
|
||||
|
||||
for (let i = 0; i < this.resources.length; i++) {
|
||||
const actionResource = this.resources[i];
|
||||
|
||||
212
pkg/harvester/dialog/HarvesterAddHotplugNic.vue
Normal file
212
pkg/harvester/dialog/HarvesterAddHotplugNic.vue
Normal file
@ -0,0 +1,212 @@
|
||||
<script>
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import { NETWORK_TYPE } from '../config/types';
|
||||
|
||||
export default {
|
||||
name: 'AddHotplugNic',
|
||||
|
||||
emits: ['close'],
|
||||
|
||||
components: {
|
||||
AsyncButton,
|
||||
Card,
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
Banner
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
try {
|
||||
this.allVMNetworks = await this.$store.dispatch('harvester/findAll', { type: NETWORK_ATTACHMENT });
|
||||
} catch (err) {
|
||||
this.errors = exceptionToErrorsArray(err);
|
||||
this.allVMNetworks = [];
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
interfaceName: '',
|
||||
networkName: '',
|
||||
macAddress: '',
|
||||
allVMNetworks: [],
|
||||
errors: [],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources?.[0];
|
||||
},
|
||||
|
||||
isFormValid() {
|
||||
return this.interfaceName !== '' && this.networkName !== '';
|
||||
},
|
||||
|
||||
vmNetworksOption() {
|
||||
return this.allVMNetworks
|
||||
.filter((network) => {
|
||||
const labels = network.metadata?.labels || {};
|
||||
const type = labels[HCI_ANNOTATIONS.NETWORK_TYPE];
|
||||
|
||||
const isValidType = [
|
||||
NETWORK_TYPE.L2VLAN,
|
||||
NETWORK_TYPE.UNTAGGED,
|
||||
NETWORK_TYPE.L2TRUNK_VLAN,
|
||||
].includes(type);
|
||||
|
||||
return isValidType && !network.isSystem;
|
||||
})
|
||||
.map((network) => {
|
||||
const label = network.isNotReady ? `${ network.id } (${ this.t('generic.notReady') })` : network.id;
|
||||
|
||||
return ({
|
||||
label,
|
||||
value: network.id || '',
|
||||
disabled: network.isNotReady,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.interfaceName = '';
|
||||
this.networkName = '';
|
||||
this.macAddress = '';
|
||||
this.errors = [];
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
if (!this.actionResource) {
|
||||
buttonCb(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
interfaceName: this.interfaceName,
|
||||
networkName: this.networkName
|
||||
};
|
||||
|
||||
if (this.macAddress) {
|
||||
payload.macAddress = this.macAddress;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await this.actionResource.doAction('addNic', payload);
|
||||
|
||||
if ([200, 204].includes(res?._status)) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
title: this.t('generic.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.hotplugNic.success', {
|
||||
interfaceName: this.interfaceName,
|
||||
vm: this.actionResource.nameDisplay
|
||||
})
|
||||
}, { root: true });
|
||||
|
||||
this.close();
|
||||
buttonCb(true);
|
||||
} else {
|
||||
this.errors = exceptionToErrorsArray(res);
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
this.errors = exceptionToErrorsArray(err);
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card
|
||||
ref="modal"
|
||||
name="modal"
|
||||
:show-highlight-border="false"
|
||||
>
|
||||
<template #title>
|
||||
<h4
|
||||
v-clean-html="t('harvester.modal.hotplugNic.title')"
|
||||
class="text-default-text"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<LabeledInput
|
||||
v-model:value="interfaceName"
|
||||
:label="t('generic.name')"
|
||||
required
|
||||
/>
|
||||
<LabeledSelect
|
||||
v-model:value="networkName"
|
||||
class="mt-20"
|
||||
:label="t('harvester.modal.hotplugNic.vmNetwork')"
|
||||
:options="vmNetworksOption"
|
||||
required
|
||||
/>
|
||||
<LabeledInput
|
||||
v-model:value="macAddress"
|
||||
class="mt-20"
|
||||
label-key="harvester.modal.hotplugNic.macAddress"
|
||||
:tooltip="t('harvester.modal.hotplugNic.macAddressTooltip', _, true)"
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
:label="err"
|
||||
color="error"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="actions">
|
||||
<div class="buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="btn role-secondary mr-10"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
mode="apply"
|
||||
:disabled="!isFormValid"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -11,7 +11,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
export default {
|
||||
name: 'HotplugModal',
|
||||
name: 'HotplugVolumeModal',
|
||||
|
||||
emits: ['close'],
|
||||
|
||||
@ -62,7 +62,7 @@ export default {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !pvc.attachVM;
|
||||
return true;
|
||||
})
|
||||
.map((pvc) => {
|
||||
return {
|
||||
@ -90,7 +90,7 @@ export default {
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
title: this.t('generic.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.hotplug.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
|
||||
message: this.t('harvester.modal.hotplugVolume.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
|
||||
}, { root: true });
|
||||
|
||||
this.close();
|
||||
@ -122,7 +122,7 @@ export default {
|
||||
>
|
||||
<template #title>
|
||||
<h4
|
||||
v-clean-html="t('harvester.modal.hotplug.title')"
|
||||
v-clean-html="t('harvester.modal.hotplugVolume.title')"
|
||||
class="text-default-text"
|
||||
/>
|
||||
</template>
|
||||
183
pkg/harvester/dialog/HarvesterDataMigrationDialog.vue
Normal file
183
pkg/harvester/dialog/HarvesterDataMigrationDialog.vue
Normal file
@ -0,0 +1,183 @@
|
||||
<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>
|
||||
154
pkg/harvester/dialog/HarvesterEnableNvidiaDriverToolkit.vue
Normal file
154
pkg/harvester/dialog/HarvesterEnableNvidiaDriverToolkit.vue
Normal file
@ -0,0 +1,154 @@
|
||||
<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>
|
||||
@ -1,13 +1,16 @@
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
|
||||
const VOLUME = 'volume';
|
||||
const NETWORK = 'network';
|
||||
const CDROM = 'cdrom';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterHotUnplugModal',
|
||||
name: 'HarvesterHotUnplug',
|
||||
|
||||
emits: ['close'],
|
||||
|
||||
@ -35,8 +38,43 @@ export default {
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
diskName() {
|
||||
return this.modalData.diskName;
|
||||
|
||||
name() {
|
||||
return this.modalData.name;
|
||||
},
|
||||
|
||||
isVolume() {
|
||||
return this.modalData.type === VOLUME;
|
||||
},
|
||||
|
||||
titleKey() {
|
||||
const keys = {
|
||||
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.title',
|
||||
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.title',
|
||||
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.title',
|
||||
};
|
||||
|
||||
return keys[this.modalData.type];
|
||||
},
|
||||
|
||||
actionLabelKey() {
|
||||
const keys = {
|
||||
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabels',
|
||||
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.actionLabels',
|
||||
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.actionLabels',
|
||||
};
|
||||
|
||||
return keys[this.modalData.type];
|
||||
},
|
||||
|
||||
successMessageKey() {
|
||||
const keys = {
|
||||
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.success',
|
||||
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.success',
|
||||
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.success',
|
||||
};
|
||||
|
||||
return keys[this.modalData.type];
|
||||
}
|
||||
},
|
||||
|
||||
@ -47,14 +85,22 @@ export default {
|
||||
|
||||
async save(buttonCb) {
|
||||
try {
|
||||
const res = await this.actionResource.doAction('removeVolume', { diskName: this.diskName });
|
||||
let res;
|
||||
|
||||
if (this.modalData.type === VOLUME) {
|
||||
res = await this.actionResource.doAction('removeVolume', { diskName: this.name });
|
||||
} else if (this.modalData.type === NETWORK) {
|
||||
res = await this.actionResource.doAction('removeNic', { interfaceName: this.name });
|
||||
} else {
|
||||
res = await this.actionResource.doAction('ejectCdRomVolume', { deviceName: this.name });
|
||||
}
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch(
|
||||
'growl/success',
|
||||
{
|
||||
title: this.t('generic.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.hotunplug.success', { name: this.diskName })
|
||||
message: this.t(this.successMessageKey, { name: this.name })
|
||||
},
|
||||
{ root: true }
|
||||
);
|
||||
@ -64,14 +110,14 @@ export default {
|
||||
} else {
|
||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||
|
||||
this['errors'] = error;
|
||||
this.errors = error;
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this['errors'] = message;
|
||||
this.errors = message;
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
@ -87,7 +133,7 @@ export default {
|
||||
>
|
||||
<template #title>
|
||||
<h4
|
||||
v-clean-html="t('harvester.virtualMachine.unplug.title', { name: diskName })"
|
||||
v-clean-html="t(titleKey, { name })"
|
||||
class="text-default-text"
|
||||
/>
|
||||
<Banner
|
||||
@ -111,9 +157,9 @@ export default {
|
||||
|
||||
<AsyncButton
|
||||
mode="apply"
|
||||
:action-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
||||
:waiting-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
||||
:success-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
||||
:action-label="t(actionLabelKey)"
|
||||
:waiting-label="t(actionLabelKey)"
|
||||
:success-label="t(actionLabelKey)"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
@ -132,4 +178,8 @@ export default {
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::v-deep(.card-title) {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
198
pkg/harvester/dialog/HarvesterInsertCdRomVolume.vue
Normal file
198
pkg/harvester/dialog/HarvesterInsertCdRomVolume.vue
Normal file
@ -0,0 +1,198 @@
|
||||
<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,15 +1,12 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { NODE } from '@shell/config/types';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { HCI } from '../types';
|
||||
|
||||
export default {
|
||||
emits: ['close'],
|
||||
@ -62,28 +59,46 @@ export default {
|
||||
return this.resources[0];
|
||||
},
|
||||
|
||||
vmi() {
|
||||
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;
|
||||
anyCpuPinning() {
|
||||
return this.resources.some((r) => r.isCpuPinning);
|
||||
},
|
||||
|
||||
return resource;
|
||||
vmsByNode() {
|
||||
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() {
|
||||
return this.t('harvester.virtualMachine.cpuPinning.migrationMessage');
|
||||
},
|
||||
|
||||
allVmsOnTargetNode() {
|
||||
if (!this.nodeName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.resources.every((r) => r.nodeName === this.nodeName);
|
||||
},
|
||||
|
||||
nodeNameList() {
|
||||
const nodes = this.$store.getters['harvester/all'](NODE);
|
||||
|
||||
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 isCpuPinning = this.actionResource?.isCpuPinning;
|
||||
const matchingCpuManagerConfig = !isCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
|
||||
const matchingCpuManagerConfig = !this.anyCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
|
||||
|
||||
return isNotSelfNode && isNotWitnessNode && matchingCpuManagerConfig;
|
||||
return isNotWitnessNode && matchingCpuManagerConfig;
|
||||
}).map((n) => {
|
||||
let label = n?.metadata?.name;
|
||||
const value = n?.metadata?.name;
|
||||
@ -126,7 +141,29 @@ export default {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.actionResource.doAction('migrate', { nodeName: this.nodeName }, {}, false);
|
||||
// Filter out VMs already running on the selected node
|
||||
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);
|
||||
this.close();
|
||||
@ -146,17 +183,35 @@ export default {
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
{{ t('harvester.modal.migration.title') }}
|
||||
{{ t('harvester.modal.migration.vmMigrationTitle', { count: resources.length }) }}
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<Banner
|
||||
v-if="actionResource?.isCpuPinning"
|
||||
v-if="anyCpuPinning"
|
||||
color="warning"
|
||||
: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
|
||||
v-model:value="nodeName"
|
||||
class="mt-15"
|
||||
:label="t('harvester.modal.migration.fields.nodeName.label')"
|
||||
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
|
||||
:options="nodeNameList"
|
||||
@ -183,7 +238,7 @@ export default {
|
||||
|
||||
<AsyncButton
|
||||
mode="apply"
|
||||
:disabled="!nodeName"
|
||||
:disabled="!nodeName || allVmsOnTargetNode"
|
||||
@click="apply"
|
||||
/>
|
||||
</div>
|
||||
@ -201,4 +256,16 @@ export default {
|
||||
justify-content: flex-end;
|
||||
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>
|
||||
|
||||
239
pkg/harvester/dialog/HarvesterStorageMigrationDialog.vue
Normal file
239
pkg/harvester/dialog/HarvesterStorageMigrationDialog.vue
Normal file
@ -0,0 +1,239 @@
|
||||
<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,8 +277,9 @@ export default {
|
||||
:label="t('harvester.modal.bundle.namespaces.label')"
|
||||
:clearable="true"
|
||||
:multiple="true"
|
||||
:append-to-body="false"
|
||||
:options="namespaceOptions"
|
||||
class="mb-10 label-select"
|
||||
class="mb-10 namespace-select"
|
||||
:tooltip="t('harvester.modal.bundle.namespaces.tooltip', _, true)"
|
||||
@update:value="updateNamespaces"
|
||||
/>
|
||||
@ -372,6 +373,11 @@ export default {
|
||||
padding: 10px 0;
|
||||
height: 160px;
|
||||
}
|
||||
.namespace-select {
|
||||
:deep(.vs__dropdown-menu) {
|
||||
max-height: 210px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
|
||||
@ -71,21 +71,38 @@ export default {
|
||||
},
|
||||
|
||||
data() {
|
||||
let provisioner = `${ this.value.provisioner || LONGHORN_DRIVER }`;
|
||||
|
||||
if (provisioner === LONGHORN_DRIVER) {
|
||||
provisioner = `${ provisioner }_${ this.value.provisionerVersion || LONGHORN_VERSION_V1 }`;
|
||||
}
|
||||
|
||||
return {
|
||||
provisioner,
|
||||
volumeGroupDialog: null,
|
||||
randomStr: randomStr(10).toLowerCase(),
|
||||
isOpen: false
|
||||
isOpen: false,
|
||||
};
|
||||
},
|
||||
|
||||
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() {
|
||||
const out = [];
|
||||
|
||||
@ -283,20 +300,6 @@ export default {
|
||||
},
|
||||
|
||||
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) {
|
||||
if (neu === _NEW) {
|
||||
this.value.lvmVolumeGroup = null;
|
||||
|
||||
157
pkg/harvester/edit/harvesterhci.io.host/HarvesterHugepages.vue
Normal file
157
pkg/harvester/edit/harvesterhci.io.host/HarvesterHugepages.vue
Normal file
@ -0,0 +1,157 @@
|
||||
<script>
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||
import { HCI } from '../../types';
|
||||
import { DOC } from '../../config/doc-links';
|
||||
|
||||
export const hugepagesTHPEnabledMode = [{
|
||||
label: 'Always',
|
||||
value: 'always',
|
||||
}, {
|
||||
label: 'Madvise',
|
||||
value: 'madvise',
|
||||
}, {
|
||||
label: 'Never',
|
||||
value: 'never',
|
||||
}];
|
||||
|
||||
export const hugepagesTHPShmemEnabledMode = [{
|
||||
label: 'Always',
|
||||
value: 'always',
|
||||
}, {
|
||||
label: 'Within Size',
|
||||
value: 'within_size',
|
||||
}, {
|
||||
label: 'Advise',
|
||||
value: 'advise',
|
||||
}, {
|
||||
label: 'Never',
|
||||
value: 'never',
|
||||
}, {
|
||||
label: 'Deny',
|
||||
value: 'deny',
|
||||
}, {
|
||||
label: 'Force',
|
||||
value: 'force',
|
||||
}];
|
||||
|
||||
export const hugepagesTHPDefragMode = [{
|
||||
label: 'Always',
|
||||
value: 'always',
|
||||
}, {
|
||||
label: 'Defer',
|
||||
value: 'defer',
|
||||
}, {
|
||||
label: 'Defer+Madvise',
|
||||
value: 'defer+madvise',
|
||||
}, {
|
||||
label: 'Madvise',
|
||||
value: 'madvise',
|
||||
}, {
|
||||
label: 'Never',
|
||||
value: 'never'
|
||||
}];
|
||||
|
||||
export default {
|
||||
name: 'HarvesterHugepages',
|
||||
components: { LabeledSelect },
|
||||
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
registerBeforeHook: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
docsTransparentHugepagesLink() {
|
||||
return DOC.TRANSPARENT_HUGEPAGES;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async saveHugepages() {
|
||||
this.hugepages['spec'] = this.spec;
|
||||
|
||||
await this.hugepages.save().catch((reason) => {
|
||||
if (reason?.type === 'error') {
|
||||
this.$store.dispatch('growl/error', {
|
||||
title: this.t('harvester.notification.title.error'),
|
||||
message: reason?.message,
|
||||
}, { root: true });
|
||||
|
||||
return Promise.reject(new Error('saveHugepages error'));
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
|
||||
|
||||
this.hugepages = hash.find((node) => {
|
||||
return node.id === this.node.id;
|
||||
});
|
||||
this.spec = this.hugepages.spec;
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
hugepages: {},
|
||||
spec: { transparent: {} },
|
||||
hugepagesTHPEnabledMode,
|
||||
hugepagesTHPShmemEnabledMode,
|
||||
hugepagesTHPDefragMode,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.registerBeforeHook(this.saveHugepages, 'saveHugepages');
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<hr class="divider" />
|
||||
<h3>
|
||||
<t
|
||||
k="harvester.host.hugepages.transparent.title"
|
||||
:raw="true"
|
||||
:url="docsTransparentHugepagesLink"
|
||||
/>
|
||||
</h3>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-4">
|
||||
<LabeledSelect
|
||||
v-model:value="spec.transparent.enabled"
|
||||
:label="t('harvester.host.hugepages.transparent.enabled')"
|
||||
:options="hugepagesTHPEnabledMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<LabeledSelect
|
||||
v-model:value="spec.transparent.shmemEnabled"
|
||||
:label="t('harvester.host.hugepages.transparent.shmemEnabled')"
|
||||
:options="hugepagesTHPShmemEnabledMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<LabeledSelect
|
||||
v-model:value="spec.transparent.defrag"
|
||||
:label="t('harvester.host.hugepages.transparent.defrag')"
|
||||
:options="hugepagesTHPDefragMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -28,6 +28,7 @@ import { HCI } from '../../types';
|
||||
import HarvesterDisk from './HarvesterDisk';
|
||||
import HarvesterSeeder from './HarvesterSeeder';
|
||||
import HarvesterKsmtuned from './HarvesterKsmtuned';
|
||||
import HarvesterHugepages from './HarvesterHugepages';
|
||||
import Tags from '../../components/DiskTags';
|
||||
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
@ -50,6 +51,7 @@ export default {
|
||||
ArrayListGrouped,
|
||||
HarvesterDisk,
|
||||
HarvesterKsmtuned,
|
||||
HarvesterHugepages,
|
||||
ButtonDropdown,
|
||||
KeyValue,
|
||||
Banner,
|
||||
@ -225,6 +227,12 @@ export default {
|
||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
||||
},
|
||||
|
||||
hasHugepagesSchema() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.HUGEPAGES);
|
||||
},
|
||||
|
||||
hasBlockDevicesSchema() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
@ -647,6 +655,17 @@ export default {
|
||||
</template>
|
||||
</ArrayListGrouped>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="hasHugepagesSchema"
|
||||
name="Hugepages"
|
||||
:weight="70"
|
||||
:label="t('harvester.host.tabs.hugepages')"
|
||||
>
|
||||
<HarvesterHugepages
|
||||
:node="value"
|
||||
:register-before-hook="registerBeforeHook"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="hasKsmtunedSchema"
|
||||
name="Ksmtuned"
|
||||
|
||||
@ -8,6 +8,7 @@ import FileSelector, { createOnSelected } from '@shell/components/form/FileSelec
|
||||
|
||||
import { randomStr } from '@shell/utils/string';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { getLoginAwareErrors } from '../utils/error';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEditKeypair',
|
||||
@ -63,6 +64,14 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
normalizedErrors() {
|
||||
const message = this.t('harvester.virtualMachine.genericLoginError');
|
||||
|
||||
return getLoginAwareErrors(this.errors, message);
|
||||
}
|
||||
},
|
||||
|
||||
methods: { onKeySelected: createOnSelected('publicKey') },
|
||||
};
|
||||
</script>
|
||||
@ -72,10 +81,9 @@ export default {
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:errors="errors"
|
||||
:errors="normalizedErrors"
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<div class="header mb-20">
|
||||
<FileSelector
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
@ -16,6 +17,7 @@ import { sortBy } from '@shell/utils/sort';
|
||||
import { BACKUP_TYPE } from '../config/types';
|
||||
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
||||
import { isBackupTargetSettingEmpty, isBackupTargetSettingUnavailable } from '../utils/setting';
|
||||
import CronExpressionEditorModal from '@shell/components/Cron/CronExpressionEditorModal.vue';
|
||||
|
||||
export default {
|
||||
name: 'CreateVMSchedule',
|
||||
@ -28,6 +30,7 @@ export default {
|
||||
LabeledSelect,
|
||||
MessageLink,
|
||||
Banner,
|
||||
CronExpressionEditorModal
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
@ -86,10 +89,12 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
return { settings: [] };
|
||||
return { settings: [], showModel: false };
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
backupTargetResource() {
|
||||
return this.settings.find( (O) => O.id === 'backup-target');
|
||||
},
|
||||
@ -172,6 +177,9 @@ export default {
|
||||
this.value.spec['maxFailure'] = this.value.spec.retain;
|
||||
}
|
||||
},
|
||||
openModal() {
|
||||
this.showModel = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -256,16 +264,28 @@ export default {
|
||||
:weight="99"
|
||||
class="bordered-table"
|
||||
>
|
||||
<LabeledInput
|
||||
v-model:value="value.spec.cron"
|
||||
class="mb-30"
|
||||
type="cron"
|
||||
required
|
||||
:mode="mode"
|
||||
:label="t('harvester.schedule.cron')"
|
||||
placeholder="0 * * * *"
|
||||
:disabled="isBackupTargetUnAvailable || isView"
|
||||
/>
|
||||
<div class="cronEditor">
|
||||
<LabeledInput
|
||||
v-model:value="value.spec.cron"
|
||||
class="mb-30"
|
||||
type="cron"
|
||||
required
|
||||
:mode="mode"
|
||||
:label="t('harvester.schedule.cron.label')"
|
||||
placeholder="0 * * * *"
|
||||
:disabled="isBackupTargetUnAvailable || isView"
|
||||
/>
|
||||
<button
|
||||
class="editCronBtn btn role-primary"
|
||||
@click="openModal"
|
||||
>
|
||||
{{ t('harvester.schedule.cron.editButton') }}
|
||||
</button>
|
||||
<CronExpressionEditorModal
|
||||
v-model:show="showModel"
|
||||
v-model:cron-expression="value.spec.cron"
|
||||
/>
|
||||
</div>
|
||||
<LabeledInput
|
||||
v-model:value.number="value.spec.retain"
|
||||
class="mb-30"
|
||||
@ -292,3 +312,16 @@ export default {
|
||||
</Tabbed>
|
||||
</CruResource>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cronEditor {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.editCronBtn {
|
||||
margin-bottom: 30px;
|
||||
margin-left: 10px;
|
||||
height: 60px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -6,6 +6,7 @@ import { Checkbox } from '@components/Form/Checkbox';
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { set } from '@shell/utils/object';
|
||||
import { Banner } from '@components/Banner';
|
||||
import KeyValue from '@shell/components/form/KeyValue';
|
||||
import NodeScheduling from '@shell/components/form/NodeScheduling';
|
||||
@ -22,6 +23,7 @@ import Reserved from './kubevirt.io.virtualmachine/VirtualMachineReserved';
|
||||
import Volume from './kubevirt.io.virtualmachine/VirtualMachineVolume';
|
||||
import Network from './kubevirt.io.virtualmachine/VirtualMachineNetwork';
|
||||
import CpuMemory from './kubevirt.io.virtualmachine/VirtualMachineCpuMemory';
|
||||
import CpuModel from './kubevirt.io.virtualmachine/VirtualMachineCpuModel';
|
||||
import CloudConfig from './kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
||||
import SSHKey from './kubevirt.io.virtualmachine/VirtualMachineSSHKey';
|
||||
|
||||
@ -38,6 +40,7 @@ export default {
|
||||
Network,
|
||||
Checkbox,
|
||||
CpuMemory,
|
||||
CpuModel,
|
||||
CruResource,
|
||||
CloudConfig,
|
||||
LabeledSelect,
|
||||
@ -70,12 +73,12 @@ export default {
|
||||
|
||||
return {
|
||||
templateId,
|
||||
templateValue: null,
|
||||
templateSpec: null,
|
||||
versionName: '',
|
||||
description: '',
|
||||
defaultVersion: null,
|
||||
isDefaultVersion: false,
|
||||
templateValue: null,
|
||||
templateSpec: null,
|
||||
versionName: '',
|
||||
description: '',
|
||||
defaultVersion: null,
|
||||
isDefaultVersion: false,
|
||||
};
|
||||
},
|
||||
|
||||
@ -154,6 +157,18 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateCpuModel(value) {
|
||||
if (!this.spec?.template?.spec?.domain?.cpu) {
|
||||
set(this.spec, 'template.spec.domain.cpu', {});
|
||||
}
|
||||
|
||||
if (value && value !== '') {
|
||||
set(this.spec.template.spec.domain.cpu, 'model', value);
|
||||
} else {
|
||||
delete this.spec.template.spec.domain.cpu.model;
|
||||
}
|
||||
},
|
||||
|
||||
async saveVMT(buttonCb) {
|
||||
this.parseVM();
|
||||
|
||||
@ -436,6 +451,17 @@ export default {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<CpuModel
|
||||
:value="spec.template.spec.domain.cpu?.model || ''"
|
||||
:mode="mode"
|
||||
@update:value="updateCpuModel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<a
|
||||
v-if="showAdvanced"
|
||||
|
||||
@ -9,8 +9,11 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import Conditions from '@shell/components/form/Conditions';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import jsyaml from 'js-yaml';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { get } from '@shell/utils/object';
|
||||
import { clone, get } from '@shell/utils/object';
|
||||
import { STORAGE_CLASS, LONGHORN, PV } from '@shell/config/types';
|
||||
import { sortBy } from '@shell/utils/sort';
|
||||
import { saferDump } from '@shell/utils/create-yaml';
|
||||
@ -33,6 +36,7 @@ export default {
|
||||
|
||||
components: {
|
||||
Banner,
|
||||
Checkbox,
|
||||
Tab,
|
||||
UnitInput,
|
||||
CruResource,
|
||||
@ -90,14 +94,34 @@ export default {
|
||||
source,
|
||||
storage,
|
||||
imageId,
|
||||
snapshots: [],
|
||||
images: [],
|
||||
showAdvanced: false,
|
||||
createWithDataVolume: false,
|
||||
snapshots: [],
|
||||
images: [],
|
||||
GIBIBYTE
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.registerBeforeHook(this.willSave, 'willSave');
|
||||
|
||||
if (this.mode === _CREATE) {
|
||||
const originalSaveYaml = this.value.saveYaml?.bind(this.value);
|
||||
|
||||
this.value.saveYaml = async(yaml) => {
|
||||
if (this.createWithDataVolume && this.isBlank) {
|
||||
const parsed = jsyaml.load(yaml);
|
||||
const dvObj = { ...parsed, type: 'cdi.kubevirt.io.datavolume' };
|
||||
const dataVolume = await this.$store.dispatch('harvester/create', dvObj);
|
||||
|
||||
await dataVolume.save();
|
||||
|
||||
return dataVolume;
|
||||
}
|
||||
|
||||
return originalSaveYaml(yaml);
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
@ -135,6 +159,10 @@ export default {
|
||||
return Object.values(VOLUME_MODE);
|
||||
},
|
||||
|
||||
accessModeOptions() {
|
||||
return ['ReadWriteOnce', 'ReadWriteMany', 'ReadOnlyMany'];
|
||||
},
|
||||
|
||||
imageOption() {
|
||||
return sortBy(
|
||||
this.images
|
||||
@ -275,6 +303,10 @@ export default {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
|
||||
},
|
||||
|
||||
isCreatePVCWithDataVolumeFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('createPVCWithDataVolume');
|
||||
},
|
||||
|
||||
isResizeDisabled() {
|
||||
return (
|
||||
!this.isLHV2VolExpansionFeatureEnabled &&
|
||||
@ -341,6 +373,58 @@ export default {
|
||||
|
||||
return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany'];
|
||||
},
|
||||
buildDataVolumeObj() {
|
||||
const storage = {
|
||||
storageClassName: this.value.spec.storageClassName,
|
||||
resources: { requests: { storage: this.storage } },
|
||||
};
|
||||
|
||||
if (this.showAdvanced && this.value.spec.accessModes?.length > 0) {
|
||||
storage.accessModes = this.value.spec.accessModes;
|
||||
}
|
||||
|
||||
if (this.showAdvanced && this.value.spec.volumeMode) {
|
||||
storage.volumeMode = this.value.spec.volumeMode;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'cdi.kubevirt.io.datavolume',
|
||||
apiVersion: 'cdi.kubevirt.io/v1beta1',
|
||||
kind: 'DataVolume',
|
||||
metadata: {
|
||||
name: this.value.metadata.name,
|
||||
namespace: this.value.metadata.namespace,
|
||||
annotations: this.value.metadata.annotations || {},
|
||||
labels: this.value.metadata.labels || {},
|
||||
},
|
||||
spec: {
|
||||
source: { blank: {} },
|
||||
storage,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
async save(buttonDone) {
|
||||
if (this.isCreate && this.isBlank && this.createWithDataVolume) {
|
||||
try {
|
||||
this.update();
|
||||
const dvObj = this.buildDataVolumeObj();
|
||||
const dataVolume = await this.$store.dispatch('harvester/create', dvObj);
|
||||
|
||||
await dataVolume.save();
|
||||
buttonDone(true);
|
||||
this.done();
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
|
||||
this['errors'] = exceptionToErrorsArray(error);
|
||||
buttonDone(false);
|
||||
}
|
||||
} else {
|
||||
await CreateEditView.methods.save.call(this, buttonDone);
|
||||
}
|
||||
},
|
||||
|
||||
willSave() {
|
||||
this.update();
|
||||
},
|
||||
@ -383,9 +467,17 @@ export default {
|
||||
this.update();
|
||||
},
|
||||
generateYaml() {
|
||||
const out = saferDump(this.value);
|
||||
this.update();
|
||||
|
||||
return out;
|
||||
if (this.isCreate && this.isBlank && this.createWithDataVolume) {
|
||||
return saferDump(this.buildDataVolumeObj());
|
||||
}
|
||||
|
||||
const plain = clone(this.value);
|
||||
|
||||
delete plain.saveYaml;
|
||||
|
||||
return saferDump(plain);
|
||||
},
|
||||
}
|
||||
};
|
||||
@ -458,18 +550,6 @@ export default {
|
||||
@update:value="update"
|
||||
/>
|
||||
|
||||
<LabeledSelect
|
||||
v-if="showVolumeMode"
|
||||
v-model:value="value.spec.volumeMode"
|
||||
:label="t('harvester.volume.volumeMode')"
|
||||
:options="volumeModeOptions"
|
||||
required
|
||||
:disabled="!isCreate"
|
||||
:mode="mode"
|
||||
class="mb-20"
|
||||
@update:value="update"
|
||||
/>
|
||||
|
||||
<UnitInput
|
||||
v-model:value="storage"
|
||||
:label="t('harvester.volume.size')"
|
||||
@ -490,6 +570,44 @@ export default {
|
||||
>
|
||||
<span>{{ t('harvester.volume.longhorn.disableResize') }}</span>
|
||||
</Banner>
|
||||
|
||||
<div class="row mb-20">
|
||||
<Checkbox
|
||||
v-if="isCreate && isBlank && isCreatePVCWithDataVolumeFeatureEnabled"
|
||||
v-model:value="createWithDataVolume"
|
||||
:label="t('harvester.volume.createWithDataVolume')"
|
||||
tooltip-key="harvester.volume.createWithDataVolumeTooltip"
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
v-if="isCreate && isCreatePVCWithDataVolumeFeatureEnabled"
|
||||
role="button"
|
||||
class="hand"
|
||||
@click="showAdvanced = !showAdvanced"
|
||||
>
|
||||
{{ showAdvanced ? t('harvester.volume.hideAdvanced') : t('harvester.volume.showAdvanced') }}
|
||||
</a>
|
||||
|
||||
<LabeledSelect
|
||||
v-if="showAdvanced"
|
||||
v-model:value="value.spec.accessModes"
|
||||
:label="t('harvester.volume.accessModes')"
|
||||
:options="accessModeOptions"
|
||||
:multiple="true"
|
||||
:mode="mode"
|
||||
class="mb-20 mt-20"
|
||||
@update:value="update"
|
||||
/>
|
||||
|
||||
<LabeledSelect
|
||||
v-if="showAdvanced"
|
||||
v-model:value="value.spec.volumeMode"
|
||||
:label="t('harvester.volume.volumeMode')"
|
||||
:options="volumeModeOptions"
|
||||
:mode="mode"
|
||||
class="mb-20"
|
||||
@update:value="update"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="!isCreate"
|
||||
|
||||
@ -371,7 +371,7 @@ export default {
|
||||
<div class="key">
|
||||
<input
|
||||
v-model="scope.row.value"
|
||||
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
||||
:placeholder="t('harvester.subnet.excludeIPs.placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -0,0 +1,133 @@
|
||||
<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,6 +1,5 @@
|
||||
<script>
|
||||
import InfoBox from '@shell/components/InfoBox';
|
||||
|
||||
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||
import { sortBy } from '@shell/utils/sort';
|
||||
import { clone } from '@shell/utils/object';
|
||||
@ -14,6 +13,13 @@ export default {
|
||||
components: { InfoBox, Base },
|
||||
|
||||
props: {
|
||||
vm: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'create'
|
||||
@ -32,10 +38,15 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
await this.fetchHotunplugData();
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
rows: this.addKeyId(clone(this.value)),
|
||||
nameIdx: 1
|
||||
nameIdx: 1,
|
||||
rows: this.addKeyId(clone(this.value)),
|
||||
hotunpluggableNics: new Set(),
|
||||
};
|
||||
},
|
||||
|
||||
@ -64,15 +75,57 @@ export default {
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
canCheckHotunplug() {
|
||||
return !!this.vm?.actions?.findHotunpluggableNics;
|
||||
},
|
||||
|
||||
vmState() {
|
||||
return this.vm?.stateDisplay;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(neu) {
|
||||
this.rows = neu;
|
||||
this.rows = this.mergeHotplugData(clone(neu));
|
||||
},
|
||||
|
||||
vmState(newState, oldState) {
|
||||
if (newState !== oldState) {
|
||||
this.fetchHotunplugData();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchHotunplugData() {
|
||||
if (!this.canCheckHotunplug) {
|
||||
this.rows = this.mergeHotplugData(clone(this.value));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await this.vm.doAction('findHotunpluggableNics');
|
||||
|
||||
this.hotunpluggableNics = new Set(resp?.interfaces || []);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to fetch hot-unpluggable NICs:', e);
|
||||
this.hotunpluggableNics = new Set();
|
||||
}
|
||||
|
||||
this.rows = this.mergeHotplugData(clone(this.value));
|
||||
},
|
||||
|
||||
mergeHotplugData(networks) {
|
||||
return (networks || []).map((network) => ({
|
||||
...network,
|
||||
isHotunpluggable: this.hotunpluggableNics.has(network.name),
|
||||
rowKeyId: network.rowKeyId || randomStr(10)
|
||||
}));
|
||||
},
|
||||
|
||||
add(type) {
|
||||
const name = this.generateName();
|
||||
|
||||
@ -118,7 +171,11 @@ export default {
|
||||
|
||||
update() {
|
||||
this.$emit('update:value', this.rows);
|
||||
}
|
||||
},
|
||||
|
||||
unplugNIC(network) {
|
||||
this.vm.unplugNIC(network.name);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -129,17 +186,28 @@ export default {
|
||||
v-for="(row, i) in rows"
|
||||
:key="i"
|
||||
>
|
||||
<button
|
||||
v-if="!isView"
|
||||
type="button"
|
||||
class="role-link remove-vol"
|
||||
@click="remove(row)"
|
||||
>
|
||||
<i class="icon icon-x" />
|
||||
</button>
|
||||
|
||||
<h3> {{ t('harvester.virtualMachine.network.title') }} </h3>
|
||||
|
||||
<div class="box-title mb-10">
|
||||
<h3>
|
||||
{{ t('harvester.virtualMachine.network.title') }}
|
||||
</h3>
|
||||
<button
|
||||
v-if="!isView"
|
||||
type="button"
|
||||
class="role-link btn btn-sm remove"
|
||||
@click="remove(row)"
|
||||
>
|
||||
<i class="icon icon-x" />
|
||||
</button>
|
||||
<button
|
||||
v-if="vm.hotplugNicFeatureEnabled && row.isHotunpluggable && isView"
|
||||
type="button"
|
||||
class="role-link btn btn-sm remove"
|
||||
:disabled="!canCheckHotunplug"
|
||||
@click="unplugNIC(row)"
|
||||
>
|
||||
{{ t('harvester.virtualMachine.hotUnplug.detachNIC.actionLabel') }}
|
||||
</button>
|
||||
</div>
|
||||
<Base
|
||||
v-model:value="rows[i]"
|
||||
:rows="rows"
|
||||
@ -162,16 +230,13 @@ export default {
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.infoBox{
|
||||
position: relative;
|
||||
}
|
||||
.box-title{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.remove-vol {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 16px;
|
||||
padding:0px;
|
||||
max-height: 28px;
|
||||
min-height: 28px;
|
||||
h3 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -31,6 +31,7 @@ export default {
|
||||
const _hash = {
|
||||
pciclaims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.PCI_CLAIM }),
|
||||
sriovs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOV }),
|
||||
srigpuovs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOVGPU_DEVICE }),
|
||||
};
|
||||
|
||||
await allHash(_hash);
|
||||
@ -106,19 +107,32 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
parentSriovOptions() {
|
||||
allSriovs() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const allSriovs = this.$store.getters[`${ inStore }/all`](HCI.SR_IOV) || [];
|
||||
|
||||
return allSriovs.map((sriov) => {
|
||||
return sriov.id;
|
||||
});
|
||||
return this.$store.getters[`${ inStore }/all`](HCI.SR_IOV) || [];
|
||||
},
|
||||
allSriovGPUs() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return this.$store.getters[`${ inStore }/all`](HCI.SR_IOVGPU_DEVICE) || [];
|
||||
},
|
||||
parentSriovOptions() {
|
||||
return this.allSriovs.map((sriov) => sriov.id);
|
||||
},
|
||||
parentSriovGPUOptions() {
|
||||
return this.allSriovGPUs.map((sriovgpu) => sriovgpu.id);
|
||||
},
|
||||
parentSriovLabel() {
|
||||
return HCI_ANNOTATIONS.PARENT_SRIOV;
|
||||
}
|
||||
},
|
||||
parentSriovGPULabel() {
|
||||
return HCI_ANNOTATIONS.PARENT_SRIOV_GPU;
|
||||
},
|
||||
vGPUAsPCIDeviceEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
enableGroup(rows = []) {
|
||||
const row = rows[0];
|
||||
@ -206,6 +220,15 @@ export default {
|
||||
:rows="rows"
|
||||
@change-rows="changeRows"
|
||||
/>
|
||||
<FilterBySriov
|
||||
v-if="vGPUAsPCIDeviceEnabled"
|
||||
ref="filterByParentSRIOVGPU"
|
||||
:parent-sriov-options="parentSriovGPUOptions"
|
||||
:parent-sriov-label="parentSriovGPULabel"
|
||||
:label="t('harvester.sriov.parentSriovGPU')"
|
||||
:rows="rows"
|
||||
@change-rows="changeRows"
|
||||
/>
|
||||
</template>
|
||||
</ResourceTable>
|
||||
</template>
|
||||
|
||||
@ -8,6 +8,7 @@ import { set } from '@shell/utils/object';
|
||||
import { HCI } from '../../../types';
|
||||
import DeviceList from './DeviceList';
|
||||
import CompatibilityMatrix from '../CompatibilityMatrix';
|
||||
import MessageLink from '@shell/components/MessageLink';
|
||||
|
||||
export default {
|
||||
name: 'VirtualMachinePCIDevices',
|
||||
@ -15,7 +16,8 @@ export default {
|
||||
LabeledSelect,
|
||||
DeviceList,
|
||||
CompatibilityMatrix,
|
||||
Banner
|
||||
Banner,
|
||||
MessageLink
|
||||
},
|
||||
props: {
|
||||
mode: {
|
||||
@ -54,6 +56,11 @@ export default {
|
||||
|
||||
const vmDevices = this.value?.domain?.devices?.hostDevices || [];
|
||||
const otherDevices = this.otherDevices(vmDevices).map(({ name }) => name);
|
||||
const vmDeviceNames = vmDevices.map(({ name }) => name);
|
||||
|
||||
this.pciDevices.forEach((row) => {
|
||||
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
|
||||
});
|
||||
|
||||
vmDevices.forEach(({ name, deviceName }) => {
|
||||
const checkName = (deviceName || '').split('/')?.[1];
|
||||
@ -133,6 +140,13 @@ export default {
|
||||
return inUse;
|
||||
},
|
||||
|
||||
toVGpuDevicesPage() {
|
||||
return {
|
||||
name: 'harvester-c-cluster-resource',
|
||||
params: { cluster: this.$store.getters['clusterId'], resource: HCI.VGPU_DEVICE },
|
||||
};
|
||||
},
|
||||
|
||||
devicesByNode() {
|
||||
return this.enabledDevices?.reduce((acc, device) => {
|
||||
const nodeName = device.status?.nodeName;
|
||||
@ -227,7 +241,12 @@ export default {
|
||||
<div class="row">
|
||||
<div class="col span-12">
|
||||
<Banner color="info">
|
||||
<t k="harvester.pci.howToUseDevice" />
|
||||
<MessageLink
|
||||
:to="toVGpuDevicesPage"
|
||||
prefix-label="harvester.pci.howToUseDeviceInVMCreation.prefix"
|
||||
middle-label="harvester.pci.howToUseDeviceInVMCreation.middle"
|
||||
suffix-label="harvester.pci.howToUseDeviceInVMCreation.suffix"
|
||||
/>
|
||||
</Banner>
|
||||
<Banner
|
||||
v-if="selectedDevices.length > 0"
|
||||
|
||||
@ -10,6 +10,7 @@ import { _VIEW } from '@shell/config/query-params';
|
||||
|
||||
import { NAMESPACE } from '@shell/config/types';
|
||||
import { HCI } from '../../types';
|
||||
import { getLoginAwareErrors } from '../../utils/error';
|
||||
|
||||
const _NEW = '_NEW';
|
||||
|
||||
@ -214,7 +215,9 @@ export default {
|
||||
buttonCb(true);
|
||||
this.cancel();
|
||||
} catch (err) {
|
||||
this.errors = [err.message];
|
||||
const message = this.t('harvester.virtualMachine.genericLoginError');
|
||||
|
||||
this.errors = getLoginAwareErrors(err, message);
|
||||
buttonCb(false);
|
||||
}
|
||||
},
|
||||
|
||||
@ -64,6 +64,12 @@ export default {
|
||||
value: 'status.productID',
|
||||
sort: ['status.productID', 'status.vendorID']
|
||||
},
|
||||
{
|
||||
name: 'classType',
|
||||
labelKey: 'harvester.usb.classType',
|
||||
value: 'status.classType',
|
||||
sort: ['status.classType']
|
||||
},
|
||||
];
|
||||
|
||||
if (!isSingleProduct) {
|
||||
|
||||
@ -48,6 +48,13 @@ export default {
|
||||
this[key] = res[key];
|
||||
}
|
||||
|
||||
const vmDevices = this.value?.domain?.devices?.hostDevices || [];
|
||||
const vmDeviceNames = vmDevices.map(({ name }) => name);
|
||||
|
||||
this.devices.forEach((row) => {
|
||||
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
|
||||
});
|
||||
|
||||
this.selectedDevices = (this.value?.domain?.devices?.hostDevices || [])
|
||||
.map(({ name }) => name)
|
||||
.filter((name) => this.enabledDevices.find((device) => device?.metadata?.name === name));
|
||||
|
||||
@ -46,6 +46,13 @@ export default {
|
||||
this[key] = res[key];
|
||||
}
|
||||
|
||||
const vmDevices = this.value?.domain?.devices?.gpus || [];
|
||||
const vmDeviceNames = vmDevices.map(({ name }) => name);
|
||||
|
||||
this.devices.forEach((row) => {
|
||||
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
|
||||
});
|
||||
|
||||
const vGpus = this.vm.isOff ? [
|
||||
...(this.value?.domain?.devices?.gpus || []).map(({ name }) => name),
|
||||
] : [
|
||||
|
||||
@ -13,11 +13,12 @@ import { ucFirst, randomStr } from '@shell/utils/string';
|
||||
import { removeObject } from '@shell/utils/array';
|
||||
import { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
|
||||
import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
|
||||
import { SOURCE_TYPE } from '../../../config/harvester-map';
|
||||
import { VOLUME_HOTPLUG_ACTION, SOURCE_TYPE } from '../../../config/harvester-map';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
||||
import { HCI } from '../../../types';
|
||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||
import { OFF } from '../../../models/kubevirt.io.virtualmachine';
|
||||
import { EMPTY_IMAGE } from '../../../utils/vm';
|
||||
|
||||
export default {
|
||||
emits: ['update:value'],
|
||||
@ -117,6 +118,10 @@ export default {
|
||||
return this.mode === _CREATE;
|
||||
},
|
||||
|
||||
isHotplugCdRomFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('hotplugCdRom');
|
||||
},
|
||||
|
||||
defaultStorageClass() {
|
||||
const defaultStorage = this.$store.getters['harvester/all'](STORAGE_CLASS).find((sc) => sc.isDefault);
|
||||
|
||||
@ -146,7 +151,7 @@ export default {
|
||||
value: {
|
||||
handler(neu) {
|
||||
const rows = clone(neu).map((V) => {
|
||||
if (!this.isCreate && V.source !== SOURCE_TYPE.CONTAINER && !V.newCreateId) {
|
||||
if (!this.isCreate && V.source !== SOURCE_TYPE.CONTAINER && !V.newCreateId && V.image !== EMPTY_IMAGE) {
|
||||
V.to = {
|
||||
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-namespace-id`,
|
||||
params: {
|
||||
@ -217,8 +222,48 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
unplugVolume(volume) {
|
||||
this.vm.unplugVolume(volume.name);
|
||||
canDoVolumeHotplugAction(volume) {
|
||||
if (!this.isHotplugCdRomFeatureEnabled && volume.type === 'cd-rom') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (volume.hotpluggable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return volume.type === 'cd-rom' && volume.bus === 'sata' && volume.image === EMPTY_IMAGE;
|
||||
},
|
||||
|
||||
getVolumeHotplugAction(volume) {
|
||||
if (volume.type === 'cd-rom' && volume.bus === 'sata') {
|
||||
if (volume.image === EMPTY_IMAGE) {
|
||||
return VOLUME_HOTPLUG_ACTION.INSERT_CDROM_IMAGE;
|
||||
}
|
||||
|
||||
return VOLUME_HOTPLUG_ACTION.EJECT_CDROM_IMAGE;
|
||||
}
|
||||
|
||||
return VOLUME_HOTPLUG_ACTION.DETACH_DISK;
|
||||
},
|
||||
|
||||
getVolumeHotplugActionLabel(volume) {
|
||||
const labels = {
|
||||
[VOLUME_HOTPLUG_ACTION.DETACH_DISK]: 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabel',
|
||||
[VOLUME_HOTPLUG_ACTION.INSERT_CDROM_IMAGE]: 'harvester.modal.insertCdRomVolume.actionLabel',
|
||||
[VOLUME_HOTPLUG_ACTION.EJECT_CDROM_IMAGE]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.actionLabel',
|
||||
};
|
||||
|
||||
return labels[this.getVolumeHotplugAction(volume)];
|
||||
},
|
||||
|
||||
hotplugVolume(volume) {
|
||||
const calls = {
|
||||
[VOLUME_HOTPLUG_ACTION.DETACH_DISK]: () => this.vm.unplugVolume(volume.name),
|
||||
[VOLUME_HOTPLUG_ACTION.INSERT_CDROM_IMAGE]: () => this.vm.insertCdRomVolume(volume.name),
|
||||
[VOLUME_HOTPLUG_ACTION.EJECT_CDROM_IMAGE]: () => this.vm.ejectCdRomVolume(volume.name),
|
||||
};
|
||||
|
||||
return calls[this.getVolumeHotplugAction(volume)]();
|
||||
},
|
||||
|
||||
componentFor(type) {
|
||||
@ -303,55 +348,57 @@ export default {
|
||||
v-for="(volume, i) in rows"
|
||||
:key="volume.id"
|
||||
>
|
||||
<InfoBox class="box">
|
||||
<button
|
||||
v-if="!isView"
|
||||
type="button"
|
||||
class="role-link btn btn-sm remove"
|
||||
@click="removeVolume(volume)"
|
||||
>
|
||||
<i class="icon icon-x" />
|
||||
</button>
|
||||
<button
|
||||
v-if="volume.hotpluggable && isView"
|
||||
type="button"
|
||||
class="role-link btn remove"
|
||||
@click="unplugVolume(volume)"
|
||||
>
|
||||
{{ t('harvester.virtualMachine.unplug.detachVolume') }}
|
||||
</button>
|
||||
<h3>
|
||||
<span
|
||||
v-if="volume.to && isVirtualType"
|
||||
class="title"
|
||||
>
|
||||
<router-link :to="volume.to">
|
||||
{{ t('harvester.virtualMachine.volume.edit') }} {{ headerFor(volume.source) }}
|
||||
</router-link>
|
||||
|
||||
<BadgeStateFormatter
|
||||
v-if="volume.pvc"
|
||||
class="ml-10 state"
|
||||
:arbitrary="true"
|
||||
:row="volume.pvc"
|
||||
:value="volume.pvc.state"
|
||||
/>
|
||||
<a
|
||||
v-if="dev && !!volume.pvc && !!volume.pvc.resourceExternalLink"
|
||||
v-clean-tooltip="t(volume.pvc.resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
|
||||
class="ml-5 resource-external"
|
||||
rel="nofollow noopener noreferrer"
|
||||
target="_blank"
|
||||
:href="volume.pvc.resourceExternalLink.url"
|
||||
<InfoBox>
|
||||
<div class="box-title mb-10">
|
||||
<h3>
|
||||
<span
|
||||
v-if="volume.to && isVirtualType"
|
||||
class="title"
|
||||
>
|
||||
<i class="icon icon-external-link" />
|
||||
</a>
|
||||
</span>
|
||||
<router-link :to="volume.to">
|
||||
{{ t('harvester.virtualMachine.volume.edit') }} {{ headerFor(volume.source) }}
|
||||
</router-link>
|
||||
|
||||
<span v-else>
|
||||
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
|
||||
</span>
|
||||
</h3>
|
||||
<BadgeStateFormatter
|
||||
v-if="volume.pvc"
|
||||
class="ml-10 state"
|
||||
:arbitrary="true"
|
||||
:row="volume.pvc"
|
||||
:value="volume.pvc.state"
|
||||
/>
|
||||
<a
|
||||
v-if="dev && !!volume.pvc && !!volume.pvc.resourceExternalLink"
|
||||
v-clean-tooltip="t(volume.pvc.resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
|
||||
class="ml-5 resource-external"
|
||||
rel="nofollow noopener noreferrer"
|
||||
target="_blank"
|
||||
:href="volume.pvc.resourceExternalLink.url"
|
||||
>
|
||||
<i class="icon icon-external-link" />
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
|
||||
</span>
|
||||
</h3>
|
||||
<button
|
||||
v-if="!isView"
|
||||
type="button"
|
||||
class="role-link btn btn-sm remove"
|
||||
@click="removeVolume(volume)"
|
||||
>
|
||||
<i class="icon icon-x" />
|
||||
</button>
|
||||
<button
|
||||
v-if="canDoVolumeHotplugAction(volume) && isView"
|
||||
type="button"
|
||||
class="role-link btn btn-sm remove"
|
||||
@click="hotplugVolume(volume)"
|
||||
>
|
||||
{{ t(getVolumeHotplugActionLabel(volume)) }}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<component
|
||||
:is="componentFor(volume.source)"
|
||||
@ -495,25 +542,24 @@ export default {
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.box {
|
||||
position: relative;
|
||||
.box-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.state {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.remove {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.bootOrder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -99,7 +99,6 @@ export default {
|
||||
this.allPVCs
|
||||
.filter( (pvc) => {
|
||||
let isAvailable = true;
|
||||
let isBeingUsed = false;
|
||||
|
||||
this.rows.forEach( (O) => {
|
||||
if (O.volumeName === pvc.metadata.name) {
|
||||
@ -111,17 +110,16 @@ export default {
|
||||
return false;
|
||||
}
|
||||
|
||||
// already used as image volume
|
||||
if (this.idx > 0 && pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pvc.isGoldenImageVolume) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pvc.attachVM && isAvailable && pvc.attachVM?.id === this.vm?.id && this.isEdit) {
|
||||
isBeingUsed = false;
|
||||
} else if (pvc.attachVM) {
|
||||
isBeingUsed = true;
|
||||
}
|
||||
|
||||
return isAvailable && !isBeingUsed && pvc.isAvailable;
|
||||
return isAvailable && pvc.isAvailable;
|
||||
})
|
||||
.map((pvc) => {
|
||||
return {
|
||||
|
||||
@ -14,6 +14,7 @@ import { _VIEW } from '@shell/config/query-params';
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import { ucFirst } from '@shell/utils/string';
|
||||
import { GIBIBYTE } from '../../../../utils/unit';
|
||||
import { EMPTY_IMAGE } from '../../../../utils/vm';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEditVMImage',
|
||||
@ -96,8 +97,20 @@ export default {
|
||||
return this.mode === _VIEW;
|
||||
},
|
||||
|
||||
isExistingCdrom() {
|
||||
return this.value.type === 'cd-rom' && !this.value.newCreateId;
|
||||
},
|
||||
|
||||
isEmptyImage() {
|
||||
return this.value.image === EMPTY_IMAGE;
|
||||
},
|
||||
|
||||
isHotplugCdRomFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('hotplugCdRom');
|
||||
},
|
||||
|
||||
imagesOption() {
|
||||
return this.images
|
||||
const images = this.images
|
||||
.filter((image) => {
|
||||
if (!image.isReady) return false;
|
||||
|
||||
@ -114,6 +127,19 @@ export default {
|
||||
value: image.id,
|
||||
disabled: image.isImportedImage
|
||||
}));
|
||||
|
||||
const options = [];
|
||||
|
||||
if (this.isHotplugCdRomFeatureEnabled) {
|
||||
options.push({
|
||||
label: this.t('harvester.virtualMachine.volume.emptyImage'),
|
||||
value: EMPTY_IMAGE,
|
||||
disabled: false
|
||||
});
|
||||
}
|
||||
options.push(...images);
|
||||
|
||||
return options;
|
||||
},
|
||||
|
||||
imageName() {
|
||||
@ -179,6 +205,7 @@ export default {
|
||||
'value.type'(neu) {
|
||||
if (neu === 'cd-rom') {
|
||||
this.value['bus'] = 'sata';
|
||||
this.updateHotpluggable();
|
||||
this.update();
|
||||
}
|
||||
},
|
||||
@ -221,12 +248,48 @@ export default {
|
||||
|
||||
return label;
|
||||
},
|
||||
|
||||
update() {
|
||||
this.value.hasDiskError = this.showDiskTooSmallError;
|
||||
this.$emit('update');
|
||||
},
|
||||
|
||||
updateHotpluggable() {
|
||||
if (this.value.type !== 'cd-rom') {
|
||||
this.value['hotpluggable'] = false;
|
||||
} else {
|
||||
this.value['hotpluggable'] = (this.value.bus === 'sata');
|
||||
}
|
||||
},
|
||||
|
||||
onTypeChange() {
|
||||
if (this.value.image === EMPTY_IMAGE && this.value.type !== 'cd-rom') {
|
||||
this.value['image'] = '';
|
||||
}
|
||||
|
||||
this.updateHotpluggable();
|
||||
this.update();
|
||||
},
|
||||
|
||||
onBusChange() {
|
||||
if (this.value.image === EMPTY_IMAGE && this.value.bus !== 'sata') {
|
||||
this.value['image'] = '';
|
||||
}
|
||||
|
||||
this.updateHotpluggable();
|
||||
this.update();
|
||||
},
|
||||
|
||||
onImageChange() {
|
||||
if (this.value.image === EMPTY_IMAGE) {
|
||||
this.value['type'] = 'cd-rom';
|
||||
this.value['bus'] = 'sata';
|
||||
this.value['size'] = `0${ GIBIBYTE }`;
|
||||
this.update();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const imageResource = this.$store.getters['harvester/all'](HCI.IMAGE)?.find( (I) => this.value.image === I.id);
|
||||
const isIsoImage = /iso$/i.test(imageResource?.imageSuffix);
|
||||
const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize);
|
||||
@ -239,6 +302,8 @@ export default {
|
||||
this.value['bus'] = 'virtio';
|
||||
}
|
||||
|
||||
this.updateHotpluggable();
|
||||
|
||||
if (imageSize) {
|
||||
let imageSizeGiB = Math.ceil(imageSize / 1024 / 1024 / 1024);
|
||||
|
||||
@ -256,6 +321,10 @@ export default {
|
||||
},
|
||||
|
||||
checkImageExists(imageId) {
|
||||
if (imageId === EMPTY_IMAGE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!!imageId && this.imagesOption.length > 0 && !findBy(this.imagesOption, 'value', imageId)) {
|
||||
this.$store.dispatch('growl/error', {
|
||||
title: this.$store.getters['i18n/t']('harvester.vmTemplate.tips.notExistImage.title', { name: imageId }),
|
||||
@ -283,6 +352,7 @@ export default {
|
||||
>
|
||||
<LabeledInput
|
||||
v-model:value="value.name"
|
||||
:disabled="!isCreate && isExistingCdrom"
|
||||
:label="t('harvester.fields.name')"
|
||||
required
|
||||
:mode="mode"
|
||||
@ -302,10 +372,11 @@ export default {
|
||||
>
|
||||
<LabeledSelect
|
||||
v-model:value="value.type"
|
||||
:disabled="!isCreate && isExistingCdrom"
|
||||
:label="t('harvester.fields.type')"
|
||||
:options="VOLUME_TYPE"
|
||||
:mode="mode"
|
||||
@update:value="update"
|
||||
@update:value="onTypeChange"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
@ -323,7 +394,7 @@ export default {
|
||||
>
|
||||
<LabeledSelect
|
||||
v-model:value="value.image"
|
||||
:disabled="idx === 0 && !isCreate && !value.newCreateId && isVirtualType"
|
||||
:disabled="(idx === 0 || isExistingCdrom) && (!isCreate && !value.newCreateId && isVirtualType)"
|
||||
:label="t('harvester.fields.image')"
|
||||
:options="imagesOption"
|
||||
:mode="mode"
|
||||
@ -351,7 +422,7 @@ export default {
|
||||
:label="t('harvester.fields.size')"
|
||||
:mode="mode"
|
||||
:required="validateRequired"
|
||||
:disabled="isResizeDisabled"
|
||||
:disabled="isResizeDisabled || isEmptyImage || (!isCreate && isExistingCdrom)"
|
||||
:suffix="GIBIBYTE"
|
||||
@update:value="update"
|
||||
/>
|
||||
@ -374,7 +445,8 @@ export default {
|
||||
:label="t('harvester.virtualMachine.volume.bus')"
|
||||
:mode="mode"
|
||||
:options="InterfaceOption"
|
||||
@update:value="update"
|
||||
:disabled="!isCreate && isExistingCdrom"
|
||||
@update:value="onBusChange"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import { isEqual } from 'lodash';
|
||||
import { mapGetters } from 'vuex';
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import { clone } from '@shell/utils/object';
|
||||
import { clone, set } from '@shell/utils/object';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
@ -32,6 +32,7 @@ import PciDevices from './VirtualMachinePciDevices/index';
|
||||
import AccessCredentials from './VirtualMachineAccessCredentials';
|
||||
import CloudConfig from './VirtualMachineCloudConfig';
|
||||
import CpuMemory from './VirtualMachineCpuMemory';
|
||||
import CpuModel from './VirtualMachineCpuModel';
|
||||
import Network from './VirtualMachineNetwork';
|
||||
import Volume from './VirtualMachineVolume';
|
||||
import SSHKey from './VirtualMachineSSHKey';
|
||||
@ -57,6 +58,7 @@ export default {
|
||||
SSHKey,
|
||||
Network,
|
||||
CpuMemory,
|
||||
CpuModel,
|
||||
CloudConfig,
|
||||
NodeScheduling,
|
||||
PodAffinity,
|
||||
@ -96,7 +98,6 @@ export default {
|
||||
templateVersionId: '',
|
||||
namePrefix: '',
|
||||
isSingle: true,
|
||||
useTemplate: false,
|
||||
isOpen: false,
|
||||
hostname,
|
||||
isRestartImmediately,
|
||||
@ -210,6 +211,10 @@ export default {
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
vGPUAsPCIDeviceEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
|
||||
},
|
||||
usbPassthroughEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
|
||||
},
|
||||
@ -255,6 +260,7 @@ export default {
|
||||
return volume;
|
||||
});
|
||||
|
||||
delete cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.MAC_ADDRESS];
|
||||
cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
||||
|
||||
// Update instance labels, labels and annotations
|
||||
@ -490,6 +496,7 @@ export default {
|
||||
if (this.isSingle) {
|
||||
if (!this.value.spec.template.spec.hostname) {
|
||||
this.value.spec.template.spec['hostname'] = this.value.metadata.name;
|
||||
this.spec.template.spec['hostname'] = this.value.metadata.name;
|
||||
}
|
||||
}
|
||||
|
||||
@ -537,6 +544,18 @@ export default {
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
updateCpuModel(value) {
|
||||
if (!this.spec?.template?.spec?.domain?.cpu) {
|
||||
set(this.spec, 'template.spec.domain.cpu', {});
|
||||
}
|
||||
|
||||
if (value && value !== '') {
|
||||
set(this.spec.template.spec.domain.cpu, 'model', value);
|
||||
} else {
|
||||
delete this.spec.template.spec.domain.cpu.model;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -725,7 +744,7 @@ export default {
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
v-if="enabledSriovgpu"
|
||||
v-if="enabledSriovgpu && !vGPUAsPCIDeviceEnabled"
|
||||
:label="t('harvester.tab.vGpuDevices')"
|
||||
name="vGpuDevices"
|
||||
:weight="-6"
|
||||
@ -869,6 +888,16 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<CpuModel
|
||||
v-model:value="cpuModel"
|
||||
:mode="mode"
|
||||
@update:value="updateCpuModel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<a
|
||||
v-if="showAdvanced"
|
||||
|
||||
357
pkg/harvester/edit/migration.harvesterhci.io.openstacksource.vue
Normal file
357
pkg/harvester/edit/migration.harvesterhci.io.openstacksource.vue
Normal file
@ -0,0 +1,357 @@
|
||||
<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>
|
||||
322
pkg/harvester/edit/migration.harvesterhci.io.ovasource.vue
Normal file
322
pkg/harvester/edit/migration.harvesterhci.io.ovasource.vue
Normal file
@ -0,0 +1,322 @@
|
||||
<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>
|
||||
@ -0,0 +1,570 @@
|
||||
<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>
|
||||
302
pkg/harvester/edit/migration.harvesterhci.io.vmwaresource.vue
Normal file
302
pkg/harvester/edit/migration.harvesterhci.io.vmwaresource.vue
Normal file
@ -0,0 +1,302 @@
|
||||
<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,6 +1,7 @@
|
||||
<script>
|
||||
import { NORMAN } from '@shell/config/types';
|
||||
import { HCI } from '../types';
|
||||
import { getHarvesterUser } from '../utils/auth';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@ -25,7 +26,7 @@ export default {
|
||||
},
|
||||
|
||||
data() {
|
||||
const user = this.$store.getters['auth/v3User'];
|
||||
const user = getHarvesterUser(this.$store.getters);
|
||||
|
||||
return {
|
||||
harvesterSettings: [],
|
||||
|
||||
@ -7,6 +7,7 @@ import harvesterStore from './store/harvester-store';
|
||||
import customValidators from './validators';
|
||||
import { PRODUCT_NAME } from './config/harvester';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import './styles/vue-flow.scss';
|
||||
|
||||
// Init the package
|
||||
export default function (plugin: IPlugin) {
|
||||
|
||||
@ -20,6 +20,7 @@ nav:
|
||||
Monitoring: Monitoring
|
||||
Logging: Logging
|
||||
'Monitoring and Logging': Monitoring and Logging
|
||||
vmimport: Virtual Machine Imports
|
||||
|
||||
resourceTable:
|
||||
groupBy:
|
||||
@ -122,9 +123,31 @@ harvester:
|
||||
namespace: Namespace
|
||||
message:
|
||||
success: 'Image { name } created successfully.'
|
||||
storageMigration:
|
||||
title: Storage Migration
|
||||
fields:
|
||||
sourceVolume:
|
||||
label: Source Volume
|
||||
placeholder: Select a source volume
|
||||
targetVolume:
|
||||
label: Target Volume
|
||||
placeholder: Select a target volume
|
||||
dataMigration:
|
||||
title: Data Migration
|
||||
fields:
|
||||
targetVolumeName:
|
||||
label: Target Volume Name
|
||||
placeholder: Enter a target volume name
|
||||
targetStorageClassName:
|
||||
label: Target Storage Class
|
||||
placeholder: Select a storage class
|
||||
migration:
|
||||
failedMessage: Latest migration failed!
|
||||
title: Migration
|
||||
vmMigrationTitle: '{count, plural, one {Migrating # VM} other {Migrating # VMs}}'
|
||||
selectedVMs: "The following virtual machine(s) will be migrated to the target node"
|
||||
unknownNode: (unknown node)
|
||||
alreadyOnTarget: Already on Target
|
||||
fields:
|
||||
nodeName:
|
||||
label: Target Node
|
||||
@ -154,15 +177,24 @@ harvester:
|
||||
nodeTimeout:
|
||||
label: Node Collection Timeout
|
||||
tooltip: Minutes allowed for collecting logs/configurations on nodes.<br/>See docs support-bundle-node-collection-timeout for detail.
|
||||
hotplug:
|
||||
hotplugVolume:
|
||||
success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
|
||||
title: Add Volume
|
||||
hotplugNic:
|
||||
success: 'The settings have been saved, but the network interface {interfaceName} will be attached only after the virtual machine is migrated.'
|
||||
title: Add Network Interface
|
||||
vmNetwork: Virtual Machine Network
|
||||
macAddress: MAC Address
|
||||
macAddressTooltip: If left blank, the MAC address will be automatically generated.
|
||||
insertCdRomVolume:
|
||||
success: '{ imageName } is inserted into device { deviceName }.'
|
||||
title: Insert Image
|
||||
image: Image
|
||||
actionLabel: Insert Image
|
||||
cpuMemoryHotplug:
|
||||
success: 'CPU and Memory are updated to the virtual machine { vm }.'
|
||||
title: Edit CPU and Memory
|
||||
maxResourcesMessage: 'You can increase the CPU to maximum { maxCpu }C and memory to maximum { maxMemory }.'
|
||||
hotunplug:
|
||||
success: 'Volume { name } is detached successfully.'
|
||||
snapshot:
|
||||
title: Take Snapshot
|
||||
name: Name
|
||||
@ -191,6 +223,9 @@ harvester:
|
||||
info: Info
|
||||
warning: Warning
|
||||
error: Error
|
||||
restartRequired:
|
||||
title: '{count} {count, plural, =1 {Virtual Machine is} other {Virtual Machines are}} Pending Restart'
|
||||
message: 'Please restart { vmNames } to apply updated configurations'
|
||||
action:
|
||||
createVM: Create Virtual Machine
|
||||
start: Start
|
||||
@ -217,8 +252,12 @@ harvester:
|
||||
suspendSchedule: Suspend
|
||||
restoreExistingVM: Replace Existing
|
||||
migrate: Migrate
|
||||
vmMigrate: Virtual Machine Migration
|
||||
cpuAndMemoryHotplug: Edit CPU and Memory
|
||||
abortMigration: Abort Migration
|
||||
storageMigration: Storage Migration
|
||||
cancelStorageMigration: Cancel Storage Migration
|
||||
dataMigration: Data Migration
|
||||
createTemplate: Generate Template
|
||||
enableMaintenance: Enable Maintenance Mode
|
||||
disableMaintenance: Disable Maintenance Mode
|
||||
@ -226,7 +265,8 @@ harvester:
|
||||
disableCPUManager: Disable CPU Manager
|
||||
cordon: Cordon
|
||||
uncordon: Uncordon
|
||||
addHotplug: Add Volume
|
||||
addHotplugVolume: Add Volume
|
||||
addHotplugNic: Hotplug Network Interface
|
||||
exportImage: Export Image
|
||||
viewlogs: View Logs
|
||||
cancelExpand: Cancel Expand
|
||||
@ -263,6 +303,7 @@ harvester:
|
||||
phase: Phase
|
||||
attachedVM: Attached Virtual Machine
|
||||
cpuManager: CPU Manager
|
||||
routeConnectivityTooltip: Connectivity between the VM network and the management network, which the Harvester nodes are connected to.
|
||||
fingerprint: Fingerprint
|
||||
value: Value
|
||||
actions: Actions
|
||||
@ -290,6 +331,17 @@ harvester:
|
||||
totalSnapshotQuota: Total Snapshot Quota
|
||||
storageClass: Storage Class
|
||||
restore: Restore
|
||||
vmImportSourceVm: Source VM
|
||||
vmImportSourceCluster: Source Cluster
|
||||
vmImportStatus: Import Status
|
||||
vmImportSourceVDatacenter: Datacenter
|
||||
vmImportSourceVEndpoint: Endpoint
|
||||
vmImportSourceVClusterStatus: Cluster Status
|
||||
vmImportSourceORegion: Region
|
||||
vmImportSourceOEndpoint: Endpoint
|
||||
vmImportSourceOClusterStatus: Cluster Status
|
||||
vmImportSourceOVAUrl: URL
|
||||
vmImportSourceOVAStatus: Status
|
||||
tab:
|
||||
volume: Volumes
|
||||
network: Networks
|
||||
@ -330,7 +382,14 @@ harvester:
|
||||
available: Available Devices
|
||||
compatibleNodes: Compatible Nodes
|
||||
impossibleSelection: 'There are no hosts with all of the selected devices.'
|
||||
howToUseDevice: 'Use the table below to enable PCI passthrough on each device you want to use in this virtual machine.'
|
||||
howToUseDeviceInVMCreation:
|
||||
prefix: 'Use the table below to enable PCI passthrough on each device you want to use in this virtual machine. <br>For vGPU devices, please enable them on the'
|
||||
middle: vGPU Devices
|
||||
suffix: page first.
|
||||
howToUseDevice:
|
||||
prefix: 'Select the device in the table to enable PCI passthrough. <br>For vGPU devices, please enable them on the'
|
||||
middle: vGPU Devices
|
||||
suffix: page.
|
||||
deviceInTheSameHost: 'You can only select devices on the same host.'
|
||||
oldFormatDevices:
|
||||
help: |-
|
||||
@ -352,6 +411,9 @@ harvester:
|
||||
claimError: Error enabling passthrough on {name}
|
||||
unclaimError: Error disabling passthrough on {name}
|
||||
cantUnclaim: You cannot disable passthrough on a device claimed by another user.
|
||||
detachWarning:
|
||||
title: Cannot Disable Passthrough
|
||||
message: Please detach the device from the VM and save it first before disabling passthrough.
|
||||
enableGroup: Enable Group
|
||||
disableGroup: Disable Group
|
||||
labelRequired: "This rule should not be manually altered: it ensures that the PCI devices selected for this virtual machine are available on the virtual machine's host."
|
||||
@ -397,7 +459,7 @@ harvester:
|
||||
volume:
|
||||
upperType: Volume name
|
||||
lowerType: volume name
|
||||
needImageOrExisting: 'At least an image volume or an existing root-disk volume is required!'
|
||||
needAtLeastOneBootable: 'At least one bootable volume is required!'
|
||||
image:
|
||||
ruleTip: 'The URL you have entered ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
||||
ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
||||
@ -442,6 +504,7 @@ harvester:
|
||||
storage: Storage
|
||||
labels: Labels
|
||||
ksmtuned: Ksmtuned
|
||||
hugepages: Hugepages
|
||||
seeder: Out-of-band Access
|
||||
detail:
|
||||
kvm:
|
||||
@ -514,6 +577,20 @@ harvester:
|
||||
fullScans: Full Scans
|
||||
stableNodeChains: Stable Node Chains
|
||||
stableNodeDups: Stable Node Dups
|
||||
hugepages:
|
||||
meminfo: Meminfo
|
||||
transparent:
|
||||
title: Transparent Hugepages <a href="{url}" target="_blank"><i class="icon icon-info" /></a>
|
||||
enabled: Enabled
|
||||
shmemEnabled: Shared Memory Enabled
|
||||
defrag: Defragmentation
|
||||
status:
|
||||
anon: Anonymous Hugepages (bytes)
|
||||
size: Default Hugepage Size (bytes)
|
||||
total: Total Hugepages
|
||||
free: Free Hugepages
|
||||
rsvd: Reserved Hugepages
|
||||
surp: Surplus Hugepages
|
||||
disk:
|
||||
add: Add Disk
|
||||
path:
|
||||
@ -578,10 +655,28 @@ harvester:
|
||||
virtualMachine:
|
||||
label: Virtual Machines
|
||||
osType: OS Type
|
||||
cpuModel:
|
||||
label: CPU Model
|
||||
fetchError: 'Failed to load CPU model configuration: {error}'
|
||||
optionLabel: "{modelName} ({count} {count, plural, one {node} other {nodes}})"
|
||||
hotplug:
|
||||
title: Enable CPU and memory hotplug
|
||||
tooltip: The default maximum CPU and maximum memory are {hotPlugTimes} times based on CPU and memory.
|
||||
restartVMMessage: Restart action is required for the virtual machine configuration change to take effect
|
||||
hotUnplug:
|
||||
actionLabel: Detach
|
||||
detachVolume:
|
||||
title: 'Are you sure that you want to detach volume {name}?'
|
||||
actionLabel: Detach Volume
|
||||
success: 'Volume { name } is detached successfully.'
|
||||
ejectCdRomVolume:
|
||||
title: 'Are you sure that you want to eject image from device {name}?'
|
||||
actionLabel: Eject Image
|
||||
success: 'Image from device { name } is ejected successfully.'
|
||||
detachNIC:
|
||||
title: 'Are you sure that you want to detach network interface {name}?'
|
||||
actionLabel: Detach Network Interface
|
||||
success: 'The settings have been saved, but the network interface {name} will be detached only after the virtual machine is migrated.'
|
||||
instance:
|
||||
singleInstance:
|
||||
multipleInstance:
|
||||
@ -613,11 +708,6 @@ harvester:
|
||||
title: 'Select the volume you want to delete:'
|
||||
deleteAll: Delete All
|
||||
tips: "Warn: The snapshots of the virtual machine will be deleted with virtual machine and the snapshots of volume will be deleted with volume."
|
||||
unplug:
|
||||
title: 'Are you sure that you want to detach volume {name} ?'
|
||||
actionLabel: Detach
|
||||
detachVolume:
|
||||
Detach Volume
|
||||
restartTip: |-
|
||||
{restart, select,
|
||||
true {Restart}
|
||||
@ -642,6 +732,7 @@ harvester:
|
||||
other {Start}
|
||||
} Now
|
||||
createSSHKey: Create a New...
|
||||
genericLoginError: Authentication failed. Please re-log in and try again.
|
||||
installAgent: Install guest agent
|
||||
enableUsb: Enable USB Tablet
|
||||
advancedOptions:
|
||||
@ -689,6 +780,7 @@ harvester:
|
||||
unmount:
|
||||
title: Are you sure?
|
||||
message: Are you sure you want to unmount this volume?
|
||||
emptyImage: No media
|
||||
network:
|
||||
title: Network
|
||||
addNetwork: Add Network
|
||||
@ -841,6 +933,11 @@ harvester:
|
||||
conditions: Conditions
|
||||
size: Size
|
||||
volumeMode: Volume Mode
|
||||
accessModes: Access Modes
|
||||
createWithDataVolume: Create with DataVolume
|
||||
createWithDataVolumeTooltip: Create Volume with Kubevirt/Containerized Data Importer way. It can fill accessMode/volumeMode automatically.
|
||||
showAdvanced: Show Advanced Options
|
||||
hideAdvanced: Hide Advanced Options
|
||||
source: Source
|
||||
kind: Kind
|
||||
sourceOptions:
|
||||
@ -897,6 +994,8 @@ harvester:
|
||||
checksumTip: Validate the image using the SHA512 checksum, if specified.
|
||||
tooltip:
|
||||
imported: Created automatically by the vm-import-controller
|
||||
errors:
|
||||
unsupportedBackend: 'Unsupported backend type: {backend}'
|
||||
|
||||
vmTemplate:
|
||||
label: Templates
|
||||
@ -958,7 +1057,9 @@ harvester:
|
||||
createTitle: Create Schedule
|
||||
createButtonText: Create Schedule
|
||||
scheduleType: Virtual Machine Schedule Type
|
||||
cron: Cron Schedule
|
||||
cron:
|
||||
label: Cron Schedule
|
||||
editButton: Edit
|
||||
detail:
|
||||
namespace: Namespace
|
||||
sourceVM: Source Virtual Machine
|
||||
@ -1064,6 +1165,8 @@ harvester:
|
||||
placeholder: e.g. 172.16.0.0/16
|
||||
excludeIPs:
|
||||
tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list.
|
||||
placeholder: single IP or 192.168.0.100..192.168.0.200
|
||||
|
||||
acl:
|
||||
label: Access Control List
|
||||
tooltip: The ACL to apply to this Subnet. Must be one of the ACLs in the same namespace.
|
||||
@ -1083,6 +1186,25 @@ harvester:
|
||||
banner: The supported field in ACL match can refer to <a href="https://kubeovn.github.io/docs/v1.14.x/en/guide/subnet/#subnet-acl" target="_blank">KubeOvn Subnet ACL document</a>
|
||||
|
||||
vpc:
|
||||
viewTopology: Topology
|
||||
topology:
|
||||
loading: Loading topology...
|
||||
empty: No resources found
|
||||
visibility:
|
||||
vpc: VPC
|
||||
subnets: Subnets
|
||||
overlayNetworks: Overlay Networks
|
||||
vms: VMs
|
||||
labels:
|
||||
cidr: CIDR
|
||||
provider: Provider
|
||||
type: Type
|
||||
clusterNetwork: Cluster Network
|
||||
network: Network
|
||||
subnet: Subnet
|
||||
ip: IP
|
||||
mac: MAC
|
||||
peering: Peering
|
||||
noAddonEnabled:
|
||||
prefix: The kubeovn-operator add-on is not enabled, click
|
||||
middle: here
|
||||
@ -1170,6 +1292,13 @@ harvester:
|
||||
rancherCluster:
|
||||
kubeConfig: Rancher KubeConfig
|
||||
removeUpstreamClusterWhenNamespaceIsDeleted: Remove Upstream Cluster When Namespace Is Deleted
|
||||
clusterPodSecurityStandard:
|
||||
whitelistedNamespaces:
|
||||
label: 'Whitelisted Namespaces'
|
||||
privilegedNamespaces:
|
||||
label: 'Privileged Namespaces'
|
||||
restrictedNamespaces:
|
||||
label: 'Restricted Namespaces'
|
||||
storageNetwork:
|
||||
range:
|
||||
placeholder: e.g. 172.16.0.0/24
|
||||
@ -1179,13 +1308,19 @@ harvester:
|
||||
vlan: VLAN ID
|
||||
exclude:
|
||||
label: Exclude IPs
|
||||
placeholder: e.g. 172.16.0.1
|
||||
placeholder: CIDR format, e.g. 172.16.0.10/32
|
||||
invalid: '"Exclude list" is invalid.'
|
||||
addIp: Add Exclude IP
|
||||
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
||||
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
|
||||
rwxNetwork:
|
||||
warning: 'WARNING: <br/> Any change to rwx-network requires longhorn RWX volumes detached before applying this setting.<br/>Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
||||
shareStorageNetwork: Share Storage Network
|
||||
dedicatedRwxNetwork: Dedicated RWX Network
|
||||
shareStorageNetworkWarning: The rwx-network is governed by storage-network, and changes here won't take effect until share-storage-network is set to false.
|
||||
vmForceDeletionPolicy:
|
||||
period: Period
|
||||
vmMigrationTimeout: VM Migration Timeout
|
||||
vmMigrationNetwork:
|
||||
parseError: "Failed to parse existing configuration."
|
||||
fetchError: "Failed to load required network resources: {error}. Please refresh the page or try again later."
|
||||
@ -1249,7 +1384,10 @@ harvester:
|
||||
deleteImage: Please select an image to delete.
|
||||
deleteSuccess: "{name} deleted successfully."
|
||||
imagePreloadStrategy: Image Preload Strategy
|
||||
nodeUpgradeOption: Node Upgrade Option
|
||||
restoreVM: Restore VM
|
||||
strategy: Strategy
|
||||
pauseNodes: Pause Nodes
|
||||
strategyType: Strategy Type
|
||||
concurrency: Concurrency
|
||||
harvesterMonitoring:
|
||||
@ -1265,10 +1403,20 @@ harvester:
|
||||
retention: How long to retain metrics
|
||||
retentionSize: Maximum size of metrics
|
||||
clusterRegistrationUrl:
|
||||
url: URL
|
||||
insecureSkipTLSVerify: Insecure Skip TLS Verify
|
||||
tip:
|
||||
prefix: Harvester secures cluster registration via TLS by default. If opt out "Insecure Skip TLS Verify", you must provide custom CA certificates using the
|
||||
middle: 'additional-ca'
|
||||
suffix: setting.
|
||||
message: To completely unset the imported Harvester cluster, please also remove it on the Rancher Dashboard UI via the <code> Virtualization Management </code> page.
|
||||
ntpServers:
|
||||
isNotIPV4: The address you entered is not IPv4 or host. Please enter a valid IPv4 address or a host address.
|
||||
isDuplicate: There are duplicate NTP server configurations.
|
||||
instanceManagerResources:
|
||||
parseError: "Failed to parse configuration: {error}"
|
||||
v1: "V1 Data Engine"
|
||||
v2: "V2 Data Engine"
|
||||
kubevirtMigration:
|
||||
parseError: "Failed to parse configuration: {error}"
|
||||
parallelMigrationsPerCluster: "Parallel Migrations Per Cluster"
|
||||
@ -1534,10 +1682,86 @@ harvester:
|
||||
'harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
|
||||
'harvester-system/harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
|
||||
'harvester-csi-driver-lvm': harvester-csi-driver-lvm is an add-on allowing users to create PVC through the LVM with local devices.
|
||||
'descheduler': 'The virtual machine auto balance optimizes workload scheduling by evicting pods that are not optimally placed according to administrator-defined policies.'
|
||||
|
||||
vmImport:
|
||||
titles:
|
||||
basic: Basic
|
||||
auth: Authentication
|
||||
pvc: Volume
|
||||
networking: Network Mapping
|
||||
advanced: Advanced
|
||||
labels:
|
||||
vmimport: Virtual Machine Import
|
||||
vmimportSourceVMWare: Source VMWare
|
||||
vmimportSourceOpenStack: Source OpenStack
|
||||
vmimportSourceOVA: Source OVA
|
||||
fields:
|
||||
sourceProvider: Source Provider Type
|
||||
sourceCluster: Source Cluster
|
||||
vmName: VM Name
|
||||
targetStorageClass: Target Storage Class
|
||||
sourceNetwork: Source Network Name
|
||||
destNetwork: Destination Network
|
||||
interfaceModel: Interface Model
|
||||
folder: Folder
|
||||
diskBus: Default Disk Bus
|
||||
defaultInterface: Default Network Interface
|
||||
skipPreflight: Skip Preflight Checks
|
||||
forcePowerOff: Force Power Off Source VM
|
||||
username: Username
|
||||
password: Password
|
||||
caCert: CA Certificate (PEM)
|
||||
selectSecret: Select Secret
|
||||
createSecret: Create New Credentials
|
||||
useSecret: Use Existing Secret
|
||||
none: None (Public URL)
|
||||
placeholders:
|
||||
selectCluster: Select a cluster...
|
||||
selectProviderFirst: Select a provider type first
|
||||
matchSource: Must match the name in the source cluster
|
||||
folderExample: e.g. /Datacenters/DC1/vm
|
||||
caCert: "-----BEGIN CERTIFICATE----- ..."
|
||||
options:
|
||||
useDefault: Use Default
|
||||
actions:
|
||||
addNetwork: Add Network Mapping
|
||||
remove: Remove
|
||||
errors:
|
||||
rfc1123: 'Invalid format. Name must be lowercase, alphanumeric, and cannot contain spaces (e.g. "my-vm-1"). If your Source VM name does not match this, you must rename it on the Source cluster first.'
|
||||
networkMappingRequired: Every Network Mapping row must have a Source and Destination selected.
|
||||
openstack:
|
||||
fields:
|
||||
endpoint: Identity Service Endpoint
|
||||
region: Region
|
||||
projectName: Project Name
|
||||
domainName: Domain Name
|
||||
retryCount: Upload Image Retry Count
|
||||
retryDelay: Upload Image Retry Delay
|
||||
placeholders:
|
||||
endpoint: "e.g. https://devstack/identity"
|
||||
region: e.g. RegionOne
|
||||
projectName: e.g. admin
|
||||
domainName: e.g. default
|
||||
retryCount: "Default: 30"
|
||||
retryDelay: "Default: 10"
|
||||
vmware:
|
||||
fields:
|
||||
endpoint: vCenter Endpoint
|
||||
datacenter: Datacenter
|
||||
placeholders:
|
||||
endpoint: "e.g. https://vscim/sdk"
|
||||
datacenter: e.g. DC0
|
||||
tooltips:
|
||||
datacenter: The exact name of the Datacenter object in vCenter
|
||||
ova:
|
||||
fields:
|
||||
url: URL
|
||||
httpTimeout: HTTP Timeout
|
||||
placeholders:
|
||||
url: "e.g. https://download.example.com/images/my-vm.ova"
|
||||
httpTimeout: "Default: 600"
|
||||
|
||||
rancherVcluster:
|
||||
accessRancher: Access the Rancher Dashboard
|
||||
hostname: Hostname
|
||||
@ -1554,6 +1778,8 @@ harvester:
|
||||
repository: Image Repository
|
||||
driver:
|
||||
location: Driver Location
|
||||
enable:
|
||||
title: Enable NVIDIA Driver Toolkit
|
||||
parsingSpecError:
|
||||
The field 'spec.valuesContent' has invalid format.
|
||||
usbController:
|
||||
@ -1664,7 +1890,8 @@ harvester:
|
||||
numVFs: Number Of Virtual Functions
|
||||
vfAddresses: Virtual Functions Addresses
|
||||
showMore: Show More
|
||||
parentSriov: Filter By Parent SR-IOV
|
||||
parentSriov: Filter By Parent SR-IOV Netork Device
|
||||
parentSriovGPU: Filter By Parent SR-IOV GPU Device
|
||||
|
||||
sriovgpu:
|
||||
label: SR-IOV GPU Devices
|
||||
@ -1695,7 +1922,7 @@ harvester:
|
||||
available: Available
|
||||
total: Total
|
||||
vGPUID: vGPU ID
|
||||
goSriovGPU:
|
||||
goSriovGPU:
|
||||
prefix: Please enable the supported GPU devices in
|
||||
middle: SR-IOV GPU Devices
|
||||
suffix: page to manage the vGPU MIG configurations.
|
||||
@ -1703,6 +1930,9 @@ harvester:
|
||||
vgpu:
|
||||
label: vGPU Devices
|
||||
noPermission: Please contact system administrator to add Harvester add-ons first.
|
||||
detachWarning:
|
||||
title: Cannot Disable vGPU
|
||||
message: Please detach the device from the VM and save it first before disabling this vGPU device.
|
||||
goSetting:
|
||||
prefix: The nvidia-driver-toolkit add-on is not enabled, click
|
||||
middle: here
|
||||
@ -1737,7 +1967,11 @@ harvester:
|
||||
claimError: Error enabling passthrough on {name}
|
||||
unclaimError: Error disabling passthrough on {name}
|
||||
cantUnclaim: You cannot disable passthrough on a device claimed by another user.
|
||||
detachWarning:
|
||||
title: Cannot Disable Passthrough
|
||||
message: Please detach the device from the VM and save it first before disabling passthrough.
|
||||
enablePassthroughWarning: 'Please re-enable the USB device if the device path changes in the following situations:<br/> 1) Re-plugging the USB device.<br/> 2) Rebooting the node.<br/><br/>An incorrect device path may cause passthrough to fail.'
|
||||
classType: Class Type
|
||||
|
||||
harvesterVlanConfigMigrateDialog:
|
||||
targetClusterNetwork:
|
||||
@ -1799,11 +2033,13 @@ advancedSettings:
|
||||
'harv-additional-ca': 'Custom CA root certificates for TLS validation.'
|
||||
'harv-overcommit-config': 'Resource overcommit configuration.'
|
||||
'harv-support-bundle-timeout': 'Support bundle timeout configuration in minutes, use 0 to disable the timeout.'
|
||||
'harv-support-bundle-file-name': 'Support bundle file name configuration.'
|
||||
'harv-support-bundle-expiration': 'Support bundle expiration configuration in minutes.'
|
||||
'harv-support-bundle-node-collection-timeout': 'Support bundle node collection timeout configuration in minutes.'
|
||||
'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down.
|
||||
'harv-ssl-parameters': Custom SSL Parameters for TLS validation.
|
||||
'harv-storage-network': 'Longhorn storage-network setting.'
|
||||
'harv-rwx-network': 'Configure RWX network behavior for shared or dedicated storage network usage.'
|
||||
'harv-support-bundle-namespaces': Select additional namespaces to include in the support bundle.
|
||||
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage.
|
||||
'harv-support-bundle-image': Support bundle image configuration. Find different versions in <a href="https://hub.docker.com/r/rancher/support-bundle-kit/tags" target="_blank">rancher/support-bundle-kit</a>.
|
||||
@ -1819,6 +2055,8 @@ advancedSettings:
|
||||
'harv-rancher-cluster': 'Configure Rancher cluster integration settings for guest cluster management.'
|
||||
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
|
||||
'harv-kubevirt-migration': 'Configure cluster-wide KubeVirt live migration parameters.'
|
||||
'harv-instance-manager-resources': 'Configure resource percentage reservations for Longhorn instance manager V1 and V2. Valid instance manager CPU range between 0 - 40.'
|
||||
'harv-cluster-pod-security-standard': 'Enforce Kubernetes Pod Security Standards (PSS) at the cluster level.'
|
||||
|
||||
typeLabel:
|
||||
kubevirt.io.virtualmachine: |-
|
||||
@ -1941,11 +2179,13 @@ typeLabel:
|
||||
one { PCI Device }
|
||||
other { PCI Devices }
|
||||
}
|
||||
|
||||
persistentvolumeclaim: |-
|
||||
{count, plural,
|
||||
one { Volume }
|
||||
other { Volumes }
|
||||
}
|
||||
|
||||
network.harvesterhci.io.clusternetwork: |-
|
||||
{count, plural,
|
||||
one { Cluster Network }
|
||||
@ -2000,3 +2240,23 @@ typeLabel:
|
||||
one { IP Pool }
|
||||
other { IP Pools }
|
||||
}
|
||||
migration.harvesterhci.io.openstacksource: |-
|
||||
{count, plural,
|
||||
one { OpenStack Source }
|
||||
other { OpenStack Sources }
|
||||
}
|
||||
migration.harvesterhci.io.vmwaresource: |-
|
||||
{count, plural,
|
||||
one { VMware Source }
|
||||
other { VMware Sources }
|
||||
}
|
||||
migration.harvesterhci.io.ovasource: |-
|
||||
{count, plural,
|
||||
one { OVA Source }
|
||||
other { OVA Sources }
|
||||
}
|
||||
migration.harvesterhci.io.virtualmachineimport: |-
|
||||
{count, plural,
|
||||
one { Virtual Machine Import }
|
||||
other { Virtual Machine Imports }
|
||||
}
|
||||
|
||||
@ -60,10 +60,25 @@ export default {
|
||||
return schema;
|
||||
},
|
||||
|
||||
toVGpuDevicesPage() {
|
||||
return {
|
||||
name: 'harvester-c-cluster-resource',
|
||||
params: { cluster: this.$store.getters['clusterId'], resource: HCI.VGPU_DEVICE },
|
||||
};
|
||||
},
|
||||
|
||||
vGPUAsPCIDeviceEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
|
||||
},
|
||||
|
||||
rows() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const rows = this.$store.getters[`${ inStore }/all`](HCI.PCI_DEVICE);
|
||||
|
||||
rows.forEach((row) => {
|
||||
row.allowDisable = true;
|
||||
});
|
||||
|
||||
return rows;
|
||||
}
|
||||
},
|
||||
@ -81,11 +96,23 @@ export default {
|
||||
{{ t('harvester.pci.noPCIPermission') }}
|
||||
</Banner>
|
||||
</div>
|
||||
<DeviceList
|
||||
v-else-if="hasSchema && enabledPCI"
|
||||
:devices="rows"
|
||||
:schema="schema"
|
||||
/>
|
||||
<div v-else-if="hasSchema && enabledPCI">
|
||||
<Banner
|
||||
v-if="vGPUAsPCIDeviceEnabled"
|
||||
color="info"
|
||||
>
|
||||
<MessageLink
|
||||
:to="toVGpuDevicesPage"
|
||||
prefix-label="harvester.pci.howToUseDevice.prefix"
|
||||
middle-label="harvester.pci.howToUseDevice.middle"
|
||||
suffix-label="harvester.pci.howToUseDevice.suffix"
|
||||
/>
|
||||
</Banner>
|
||||
<DeviceList
|
||||
:devices="rows"
|
||||
:schema="schema"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<Banner color="warning">
|
||||
<MessageLink
|
||||
|
||||
@ -54,7 +54,13 @@ export default {
|
||||
devices() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return this.$store.getters[`${ inStore }/all`](HCI.USB_DEVICE) || [];
|
||||
const data = this.$store.getters[`${ inStore }/all`](HCI.USB_DEVICE) || [];
|
||||
|
||||
data.forEach((row) => {
|
||||
row.allowDisable = true;
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -65,6 +65,10 @@ export default {
|
||||
const vGpuDevices = this.$store.getters[`${ inStore }/all`](HCI.VGPU_DEVICE) || [];
|
||||
const srioVGpuDevices = this.$store.getters[`${ inStore }/all`](HCI.SR_IOVGPU_DEVICE) || [];
|
||||
|
||||
vGpuDevices.forEach((row) => {
|
||||
row.allowDisable = true;
|
||||
});
|
||||
|
||||
if (this.hasSRIOVGPUSchema) {
|
||||
return vGpuDevices.filter((device) => !!srioVGpuDevices.find((s) => s.isEnabled && s.spec?.nodeName === device.spec?.nodeName));
|
||||
}
|
||||
|
||||
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