mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-01 03:36:04 +08:00
Compare commits
27
Commits
v1.8.2
...
v1.6.0-rc3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f19494d98 | ||
|
|
296601d38c | ||
|
|
5d9ba584b8 | ||
|
|
eab4ba3d0f | ||
|
|
4e5320dc72 | ||
|
|
ba7092f400 | ||
|
|
eec7c3573d | ||
|
|
f0014cc627 | ||
|
|
9aa311a7e3 | ||
|
|
013d9f1d7e | ||
|
|
8c044ba44c | ||
|
|
7b8592e07d | ||
|
|
53d2e67b4f | ||
|
|
1d146a40bd | ||
|
|
11d6943596 | ||
|
|
7b754457ec | ||
|
|
2cb838eded | ||
|
|
bdcb34ab14 | ||
|
|
8daf44b3f6 | ||
|
|
8044e74102 | ||
|
|
264d65cc6d | ||
|
|
342a82c8e0 | ||
|
|
e7cd5cc4b0 | ||
|
|
cf405be71e | ||
|
|
911d30d7ba | ||
|
|
1b4bfa0962 | ||
|
|
d0f6f18a8a |
@@ -1,4 +1,4 @@
|
|||||||
name: "[PR Management] Auto Assign Reviewer"
|
name: "[PR Management] Auto Assign Reviewer & Assignee"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
@@ -9,8 +9,9 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
auto-assign:
|
auto-assign:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: rancher/gh-issue-mgr/auto-assign-action@main
|
- uses: rancher/gh-issue-mgr/auto-assign-action@main
|
||||||
with:
|
with:
|
||||||
configuration-path: .github/auto-assign-config.yaml
|
configuration-path: .github/auto-assign-config.yaml
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
name: "[PR Management] Add Labels"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, reopened]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'release-harvester-v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
add-require-backport-label:
|
||||||
|
if: github.event.pull_request.draft == false &&
|
||||||
|
github.event.pull_request.base.ref == 'main'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.base_ref }}
|
||||||
|
|
||||||
|
- name: Fetch release branches and PR labels
|
||||||
|
id: fetch_info
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo="${{ github.repository }}"
|
||||||
|
pr_number=${{ github.event.pull_request.number }}
|
||||||
|
|
||||||
|
release_branches=$(gh api "repos/${repo}/branches" --paginate --jq '.[].name' | grep -E '^release-harvester-v[0-9]+\.[0-9]+$' || true)
|
||||||
|
|
||||||
|
if [[ -z "$release_branches" ]]; then
|
||||||
|
echo "should_label=false" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
latest_branch=$(echo "$release_branches" | sort -Vr | head -n1)
|
||||||
|
version="${latest_branch#release-harvester-v}"
|
||||||
|
release_tag="v${version}.0"
|
||||||
|
|
||||||
|
tags=$(gh api "repos/${repo}/releases" --paginate --jq '.[].tag_name')
|
||||||
|
if echo "$tags" | grep -Fxq "$release_tag"; then
|
||||||
|
echo "should_label=false" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
label="require backport/v${version}"
|
||||||
|
echo "should_label=true" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "backport_label=$label" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
pr_labels=$(gh pr view "$pr_number" --repo "$repo" --json labels --jq '.labels[].name' || echo "")
|
||||||
|
pr_labels_csv=$(echo "$pr_labels" | tr '\n' ',' | sed 's/,$//')
|
||||||
|
echo "pr_labels=$pr_labels_csv" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Add label if needed
|
||||||
|
if: steps.fetch_info.outputs.should_label == 'true' && !contains(steps.fetch_info.outputs.pr_labels, steps.fetch_info.outputs.backport_label)
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
echo "Adding label: ${{ steps.fetch_info.outputs.backport_label }}"
|
||||||
|
gh pr edit ${{ github.event.pull_request.number }} \
|
||||||
|
--repo ${{ github.repository }} \
|
||||||
|
--add-label "${{ steps.fetch_info.outputs.backport_label }}"
|
||||||
|
|
||||||
|
add-backport-label:
|
||||||
|
if: github.event.pull_request.draft == false &&
|
||||||
|
startsWith(github.event.pull_request.base.ref, 'release-harvester-v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check conditions for backport label
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
IS_MERGIFY=$(echo '${{ github.event.pull_request.user.login }}' | grep -iq 'mergify' && echo true || echo false)
|
||||||
|
TARGET_BRANCH=${{ github.event.pull_request.base.ref }}
|
||||||
|
|
||||||
|
echo "IS_MERGIFY=$IS_MERGIFY" >> $GITHUB_OUTPUT
|
||||||
|
echo "TARGET_BRANCH=$TARGET_BRANCH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Add label if needed
|
||||||
|
if: steps.check.outputs.IS_MERGIFY == 'true' && startsWith(steps.check.outputs.TARGET_BRANCH, 'release-harvester-v')
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
TARGET_BRANCH="${{ steps.check.outputs.TARGET_BRANCH }}"
|
||||||
|
version="${TARGET_BRANCH#release-harvester-v}"
|
||||||
|
label="backport/v${version}"
|
||||||
|
echo "Adding label $label"
|
||||||
|
gh pr edit ${{ github.event.pull_request.number }} \
|
||||||
|
--repo ${{ github.repository }} \
|
||||||
|
--add-label "$label"
|
||||||
@@ -11,18 +11,19 @@ defaults:
|
|||||||
working-directory: ./
|
working-directory: ./
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
check-version:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout repository
|
||||||
with:
|
uses: actions/checkout@v4
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- name: Run tests
|
- name: Check package version
|
||||||
uses: ./.github/actions/lint
|
env:
|
||||||
|
TAG_VERSION: ${{ github.event.release.tag_name }}
|
||||||
|
run: ./scripts/check-version.sh $TAG_VERSION
|
||||||
|
|
||||||
build-and-push-extension-catalog:
|
build-and-push-extension-catalog:
|
||||||
needs: lint
|
needs: check-version
|
||||||
uses: ./.github/workflows/build-extension-catalog.yml
|
uses: ./.github/workflows/build-extension-catalog.yml
|
||||||
permissions:
|
permissions:
|
||||||
actions: write
|
actions: write
|
||||||
|
|||||||
@@ -22,26 +22,27 @@ jobs:
|
|||||||
- name: Determine release tag
|
- name: Determine release tag
|
||||||
id: determine_tag
|
id: determine_tag
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.ref_name }}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
if [[ "${{ github.event.release.tag_name }}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
||||||
RELEASE_TAG="harvester-${BASH_REMATCH[1]}"
|
RELEASE_TAG="harvester-${BASH_REMATCH[1]}"
|
||||||
echo "${RELEASE_TAG}"
|
echo "${RELEASE_TAG}"
|
||||||
echo "release_tag=${RELEASE_TAG}" >> $GITHUB_OUTPUT
|
echo "release_tag=${RELEASE_TAG}" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo "Error: invalid tag format." && exit 1
|
echo "Error: invalid tag format." && exit 1
|
||||||
fi
|
fi
|
||||||
lint:
|
check-version:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout repository
|
||||||
with:
|
uses: actions/checkout@v4
|
||||||
fetch-depth: 1
|
- name: Check package version
|
||||||
|
env:
|
||||||
|
TAG_VERSION: ${{ github.event.release.tag_name }}
|
||||||
|
run: ./scripts/check-version.sh $TAG_VERSION
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
uses: ./.github/actions/lint
|
|
||||||
build-extension-charts:
|
build-extension-charts:
|
||||||
needs:
|
needs:
|
||||||
- setup-release-tag
|
- setup-release-tag
|
||||||
- lint
|
- check-version
|
||||||
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
|
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
|
||||||
permissions:
|
permissions:
|
||||||
actions: write
|
actions: write
|
||||||
|
|||||||
@@ -5,14 +5,20 @@ on:
|
|||||||
- v[1-9].*
|
- v[1-9].*
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-validation:
|
check-version:
|
||||||
name: Build Test
|
runs-on: ubuntu-latest
|
||||||
uses: ./.github/workflows/run-lint.yaml
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Check package version
|
||||||
|
env:
|
||||||
|
TAG_VERSION: ${{github.ref_name}}
|
||||||
|
run: ./scripts/check-version.sh $TAG_VERSION
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build and Upload Package
|
name: Build and Upload Package
|
||||||
uses: ./.github/workflows/build-and-publish-standalone.yaml
|
uses: ./.github/workflows/build-and-publish-standalone.yaml
|
||||||
needs:
|
needs: check-version
|
||||||
- build-validation
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ To validate changes in a release series, switch to the appropriate `vX.Y-head` b
|
|||||||
- Examples:
|
- Examples:
|
||||||
- Test `1.0.x` series → `v1.0-head`
|
- Test `1.0.x` series → `v1.0-head`
|
||||||
- Test `1.5.x` series → `v1.5-head`
|
- Test `1.5.x` series → `v1.5-head`
|
||||||
|
- Test `1.6.x` series → `v1.6-head`
|
||||||
|
|
||||||
**Steps:**
|
**Steps:**
|
||||||
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
|
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
'type-case': [2, 'always', 'lower-case'],
|
'type-case': [2, 'always', 'lower-case'],
|
||||||
'type-empty': [2, 'never'],
|
'type-empty': [2, 'never'],
|
||||||
'subject-case': [2, 'always', 'lower-case'],
|
'subject-case': [0, 'never'],
|
||||||
'subject-empty': [2, 'never'],
|
'subject-empty': [2, 'never'],
|
||||||
'subject-full-stop': [2, 'never', '.'],
|
'subject-full-stop': [2, 'never', '.'],
|
||||||
'subject-max-length': [2, 'always', 72],
|
'subject-max-length': [0, 'never'],
|
||||||
'body-leading-blank': [2, 'always'],
|
'body-leading-blank': [2, 'always'],
|
||||||
'body-max-line-length': [2, 'always', 100],
|
'body-max-line-length': [2, 'always', 100],
|
||||||
'footer-leading-blank': [2, 'always'],
|
'footer-leading-blank': [2, 'always'],
|
||||||
|
|||||||
+2
-2
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.6.0-dev",
|
"version": "1.6.0-rc3",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-class-static-block": "7.26.0",
|
"@babel/plugin-transform-class-static-block": "7.26.0",
|
||||||
"@rancher/shell": "3.0.5-rc.3",
|
"@rancher/shell": "3.0.5-rc.6",
|
||||||
"cache-loader": "^4.1.0",
|
"cache-loader": "^4.1.0",
|
||||||
"color": "4.2.3",
|
"color": "4.2.3",
|
||||||
"ip": "2.0.1",
|
"ip": "2.0.1",
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { mapGetters } from 'vuex';
|
|||||||
import ActionMenu from '@shell/components/ActionMenuShell';
|
import ActionMenu from '@shell/components/ActionMenuShell';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
import { HCI_ALLOWED_SETTINGS, HCI_SETTING } from '../config/settings';
|
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
|
||||||
|
import { DOC } from '../config/doc-links';
|
||||||
|
import { docLink } from '../utils/feature-flags';
|
||||||
|
|
||||||
const CATEGORY = {
|
const CATEGORY = {
|
||||||
ui: [
|
ui: [
|
||||||
@@ -96,7 +98,7 @@ export default {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const description = this.t(setting.description, {}, true)?.toLowerCase() || '';
|
const description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || '';
|
||||||
|
|
||||||
// filter by description
|
// filter by description
|
||||||
if (description.includes(searchQuery)) {
|
if (description.includes(searchQuery)) {
|
||||||
@@ -193,6 +195,19 @@ export default {
|
|||||||
}
|
}
|
||||||
buttonDone(false);
|
buttonDone(false);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getDocLinkParams(setting) {
|
||||||
|
const settingConfig = HCI_ALLOWED_SETTINGS[setting.id] || HCI_SINGLE_CLUSTER_ALLOWED_SETTING[setting.id];
|
||||||
|
|
||||||
|
if (settingConfig?.docPath) {
|
||||||
|
const version = this.$store.getters['harvester-common/getServerVersion']();
|
||||||
|
const url = docLink(DOC[settingConfig.docPath], version);
|
||||||
|
|
||||||
|
return { url };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -223,7 +238,7 @@ export default {
|
|||||||
Experimental
|
Experimental
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<h2 v-clean-html="t(setting.description, {}, true)">
|
<h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)">
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -0,0 +1,294 @@
|
|||||||
|
<script>
|
||||||
|
import { _EDIT } from '@shell/config/query-params';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import InfoBox from '@shell/components/InfoBox';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { CSI_DRIVER } from '../../types';
|
||||||
|
import { LONGHORN_DRIVER } from '@shell/config/types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CSIOnlineExpandValidation',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Banner,
|
||||||
|
InfoBox,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await allHash({ csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }) });
|
||||||
|
this.fetchError = null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch CSI drivers:', error); // eslint-disable-line no-console
|
||||||
|
this.fetchError = this.t(
|
||||||
|
'harvester.setting.csiOnlineExpandValidation.failedToLoadDrivers',
|
||||||
|
{ error: error.message || error },
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
configArr: [],
|
||||||
|
parseError: null,
|
||||||
|
fetchError: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
const initValue = this.value.value || this.value.default || '{}';
|
||||||
|
|
||||||
|
this.configArr = this.parseValue(initValue);
|
||||||
|
this.registerBeforeHook?.(this.willSave, 'willSave');
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
allErrors() {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (this.fetchError) {
|
||||||
|
errors.push(this.fetchError);
|
||||||
|
}
|
||||||
|
if (this.parseError) {
|
||||||
|
errors.push(this.parseError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
},
|
||||||
|
|
||||||
|
csiDrivers() {
|
||||||
|
if (this.fetchError) return [];
|
||||||
|
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
provisioners() {
|
||||||
|
const usedKeys = this.configArr.map(({ key }) => key);
|
||||||
|
|
||||||
|
return this.csiDrivers
|
||||||
|
.filter(({ name }) => !usedKeys.includes(name))
|
||||||
|
.map(({ name }) => name);
|
||||||
|
},
|
||||||
|
|
||||||
|
provisionerValue() {
|
||||||
|
return [
|
||||||
|
{ label: 'True', value: true },
|
||||||
|
{ label: 'False', value: false },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
disableAdd() {
|
||||||
|
return this.parseError || this.fetchError || this.configArr.length >= this.csiDrivers.length;
|
||||||
|
},
|
||||||
|
|
||||||
|
disableConfigEditing() {
|
||||||
|
return this.parseError || this.fetchError;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'value.value'(newVal, oldVal) {
|
||||||
|
if (newVal !== oldVal) {
|
||||||
|
this.configArr = this.parseValue(newVal || '{}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
_convertToBoolean(value) {
|
||||||
|
if (typeof value === 'boolean') return value;
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const lowerCaseValue = value.toLowerCase();
|
||||||
|
|
||||||
|
if (lowerCaseValue === 'true') return true;
|
||||||
|
if (lowerCaseValue === 'false') return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // default to false for any other string or non-boolean type
|
||||||
|
},
|
||||||
|
parseValue(raw) {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(raw);
|
||||||
|
|
||||||
|
this.parseError = null;
|
||||||
|
|
||||||
|
return Object.entries(json).map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
value: this._convertToBoolean(value),
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[CSIOnlineExpandValidation] JSON Parsing Error:', raw, e); // eslint-disable-line no-console
|
||||||
|
this.parseError = this.t(
|
||||||
|
'harvester.setting.csiOnlineExpandValidation.invalidJsonFormat',
|
||||||
|
{ error: e.message },
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
stringifyConfig() {
|
||||||
|
const obj = {};
|
||||||
|
|
||||||
|
this.configArr.forEach(({ key, value }) => {
|
||||||
|
obj[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.configArr.length ? JSON.stringify(obj) : '';
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.value.value = this.stringifyConfig();
|
||||||
|
},
|
||||||
|
|
||||||
|
willSave() {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
this.configArr.forEach(({ key }) => {
|
||||||
|
if (!key) {
|
||||||
|
errors.push(
|
||||||
|
this.t('validation.required', { key: this.t('harvester.setting.csiOnlineExpandValidation.provisioner') }, true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.value.value = this.stringifyConfig();
|
||||||
|
|
||||||
|
return errors.length ? Promise.reject(errors) : Promise.resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
this.configArr = this.parseValue(this.value.default || '{}');
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
disableEdit(driverKey) {
|
||||||
|
return this.fetchError || driverKey === LONGHORN_DRIVER;
|
||||||
|
},
|
||||||
|
|
||||||
|
add() {
|
||||||
|
if (this.disableConfigEditing) return;
|
||||||
|
|
||||||
|
this.configArr.push({ key: '', value: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(index) {
|
||||||
|
if (this.disableConfigEditing) return;
|
||||||
|
|
||||||
|
this.configArr.splice(index, 1);
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
onValueChange(idx, newVal) {
|
||||||
|
if (this.disableConfigEditing) return;
|
||||||
|
|
||||||
|
const val = newVal === 'true' ? true : newVal === 'false' ? false : newVal;
|
||||||
|
|
||||||
|
this.configArr[idx].value = val;
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Banner
|
||||||
|
v-for="(errorMsg, index) in allErrors"
|
||||||
|
:key="index"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{{ errorMsg }}
|
||||||
|
</Banner>
|
||||||
|
<InfoBox
|
||||||
|
v-for="(driver, idx) in configArr"
|
||||||
|
:key="idx"
|
||||||
|
class="box"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="role-link btn btn-sm remove"
|
||||||
|
type="button"
|
||||||
|
:disabled="disableEdit(driver.key)"
|
||||||
|
@click="remove(idx)"
|
||||||
|
>
|
||||||
|
<i class="icon icon-x" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="driver.key"
|
||||||
|
label-key="harvester.setting.csiOnlineExpandValidation.provisioner"
|
||||||
|
required
|
||||||
|
searchable
|
||||||
|
:mode="mode"
|
||||||
|
:disabled="disableEdit(driver.key)"
|
||||||
|
:options="provisioners"
|
||||||
|
@update:value="update"
|
||||||
|
@keydown.native.enter.prevent
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="driver.value"
|
||||||
|
:value="driver.value.toString()"
|
||||||
|
label-key="harvester.setting.csiOnlineExpandValidation.value"
|
||||||
|
required
|
||||||
|
searchable
|
||||||
|
:mode="mode"
|
||||||
|
:disabled="disableEdit(driver.key)"
|
||||||
|
:options="provisionerValue"
|
||||||
|
@update:value="val => onValueChange(idx, val)"
|
||||||
|
@keydown.native.enter.prevent
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</InfoBox>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn-sm role-primary"
|
||||||
|
:disabled="disableAdd"
|
||||||
|
@click="add"
|
||||||
|
>
|
||||||
|
{{ t('generic.add') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.box {
|
||||||
|
position: relative;
|
||||||
|
padding-top: 40px;
|
||||||
|
}
|
||||||
|
.remove {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
<script>
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import { SECRET } from '@shell/config/types';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import FileSelector, { createOnSelected } from '@shell/components/form/FileSelector';
|
||||||
|
import YamlEditor from '@shell/components/YamlEditor';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterRancherCluster',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
RadioGroup,
|
||||||
|
FileSelector,
|
||||||
|
YamlEditor
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: false,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let parseDefaultValue = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = this.value.value || this.value.default || '{}';
|
||||||
|
const parsed = JSON.parse(data);
|
||||||
|
|
||||||
|
parseDefaultValue = {
|
||||||
|
kubeConfig: '',
|
||||||
|
removeUpstreamClusterWhenNamespaceIsDeleted: parsed.removeUpstreamClusterWhenNamespaceIsDeleted || false
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
parseDefaultValue = {
|
||||||
|
kubeConfig: '',
|
||||||
|
removeUpstreamClusterWhenNamespaceIsDeleted: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
parseDefaultValue,
|
||||||
|
errors: [],
|
||||||
|
existingSecret: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
await this.checkExistingSecret();
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.willSave, 'willSave');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onKeySelected: createOnSelected('parseDefaultValue.kubeConfig'),
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (this.parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted) {
|
||||||
|
this.value['value'] = JSON.stringify({ removeUpstreamClusterWhenNamespaceIsDeleted: true });
|
||||||
|
} else {
|
||||||
|
this.value['value'] = this.value.default || '{}';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async checkExistingSecret() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
|
||||||
|
const secrets = this.$store.getters[`${ inStore }/all`](SECRET) || [];
|
||||||
|
|
||||||
|
this.existingSecret = secrets.find((secret) => secret.metadata.name === 'rancher-cluster-config' &&
|
||||||
|
secret.metadata.namespace === 'harvester-system'
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the secret exists and has data, populate the kubeConfig
|
||||||
|
if (this.existingSecret?.data?.kubeConfig) {
|
||||||
|
const decodedContent = atob(this.existingSecret.data.kubeConfig);
|
||||||
|
|
||||||
|
this.parseDefaultValue.kubeConfig = decodedContent;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.update();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createOrUpdateRancherKubeConfigSecret() {
|
||||||
|
this.errors = [];
|
||||||
|
// Check if kubeConfig is provided
|
||||||
|
if (!this.parseDefaultValue.kubeConfig) {
|
||||||
|
this.errors.push(this.t('validation.required', { key: this.t('harvester.setting.rancherCluster.kubeConfig') }, true));
|
||||||
|
|
||||||
|
return Promise.reject(this.errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let secret;
|
||||||
|
|
||||||
|
if (this.existingSecret) {
|
||||||
|
secret = this.existingSecret;
|
||||||
|
secret.setData('kubeConfig', this.parseDefaultValue.kubeConfig);
|
||||||
|
} else {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
secret = await this.$store.dispatch(`${ inStore }/create`, {
|
||||||
|
apiVersion: 'v1',
|
||||||
|
kind: 'Secret',
|
||||||
|
metadata: {
|
||||||
|
name: 'rancher-cluster-config',
|
||||||
|
namespace: 'harvester-system'
|
||||||
|
},
|
||||||
|
type: 'secret',
|
||||||
|
data: { kubeConfig: btoa(this.parseDefaultValue.kubeConfig) }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await secret.save();
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
} catch (err) {
|
||||||
|
this.errors = exceptionToErrorsArray(err);
|
||||||
|
|
||||||
|
return Promise.reject(this.errors);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteRancherKubeConfigSecret() {
|
||||||
|
if (this.existingSecret) {
|
||||||
|
this.existingSecret.remove();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async willSave() {
|
||||||
|
// Only create or update secret if enabled
|
||||||
|
if (this.parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted) {
|
||||||
|
await this.createOrUpdateRancherKubeConfigSecret();
|
||||||
|
} else {
|
||||||
|
await this.deleteRancherKubeConfigSecret();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
this.parseDefaultValue = {
|
||||||
|
kubeConfig: '',
|
||||||
|
removeUpstreamClusterWhenNamespaceIsDeleted: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted'(val, oldVal) {
|
||||||
|
if (val && !oldVal && this.existingSecret?.data?.kubeConfig) {
|
||||||
|
// Populate kubeConfig with the existing secret value
|
||||||
|
this.parseDefaultValue.kubeConfig = atob(this.existingSecret.data.kubeConfig);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'parseDefaultValue.kubeConfig'(val) {
|
||||||
|
this.$refs.yaml?.updateValue(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted"
|
||||||
|
:label="t('harvester.setting.rancherCluster.removeUpstreamClusterWhenNamespaceIsDeleted')"
|
||||||
|
name="removeUpstreamClusterWhenNamespaceIsDeleted"
|
||||||
|
:options="[true, false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted"
|
||||||
|
class="row mt-20"
|
||||||
|
>
|
||||||
|
<div class="col span-12">
|
||||||
|
<FileSelector
|
||||||
|
class="btn btn-sm bg-primary mb-10"
|
||||||
|
:label="t('generic.readFromFile')"
|
||||||
|
@selected="onKeySelected"
|
||||||
|
/>
|
||||||
|
<YamlEditor
|
||||||
|
ref="yaml"
|
||||||
|
v-model:value="parseDefaultValue.kubeConfig"
|
||||||
|
class="yaml-editor"
|
||||||
|
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$yaml-height: 540px;
|
||||||
|
|
||||||
|
:deep() .yaml-editor{
|
||||||
|
flex: 1;
|
||||||
|
min-height: $yaml-height;
|
||||||
|
& .code-mirror .CodeMirror {
|
||||||
|
position: initial;
|
||||||
|
height: auto;
|
||||||
|
min-height: $yaml-height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,6 +3,9 @@ import { NAMESPACE } from '@shell/config/types';
|
|||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
|
const SELECT_ALL = 'select_all';
|
||||||
|
const UNSELECT_ALL = 'unselect_all';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterBundleNamespaces',
|
name: 'HarvesterBundleNamespaces',
|
||||||
|
|
||||||
@@ -11,47 +14,76 @@ export default {
|
|||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
async fetch() {
|
async fetch() {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
||||||
|
const response = await this.$store.dispatch('harvester/request', { url });
|
||||||
|
|
||||||
|
this.defaultNamespaces = response.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
this.defaultNamespaces = [];
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
let namespaces = [];
|
|
||||||
const namespacesStr = this.value?.value || this.value?.default || '';
|
const namespacesStr = this.value?.value || this.value?.default || '';
|
||||||
|
const namespaces = namespacesStr ? namespacesStr.split(',') : [];
|
||||||
|
|
||||||
if (namespacesStr) {
|
return {
|
||||||
namespaces = namespacesStr.split(',');
|
namespaces,
|
||||||
}
|
defaultNamespaces: [],
|
||||||
|
loading: true
|
||||||
return { namespaces };
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
allNamespaces() {
|
||||||
|
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
filteredNamespaces() {
|
||||||
|
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
|
||||||
|
|
||||||
|
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
|
||||||
|
},
|
||||||
|
|
||||||
namespaceOptions() {
|
namespaceOptions() {
|
||||||
return this.$store.getters['harvester/all'](NAMESPACE).map((N) => {
|
if (this.availableNamespaces.length === 0) return [];
|
||||||
return {
|
|
||||||
label: N.id,
|
const allSelected =
|
||||||
value: N.id
|
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];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
update() {
|
update(selected) {
|
||||||
const namespaceStr = this.namespaces.join(',');
|
if (selected.includes(SELECT_ALL)) {
|
||||||
|
this.namespaces = [...this.filteredNamespaces];
|
||||||
|
} else if (selected.includes(UNSELECT_ALL)) {
|
||||||
|
this.namespaces = [];
|
||||||
|
} else {
|
||||||
|
this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL);
|
||||||
|
}
|
||||||
|
|
||||||
this.value['value'] = namespaceStr;
|
this.value.value = this.namespaces.join(',');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
'value.value': {
|
'value.value'(newVal) {
|
||||||
handler(neu) {
|
const raw = newVal || this.value.default || '';
|
||||||
if (neu === this.value.default || !neu) {
|
|
||||||
this.namespaces = [];
|
this.namespaces = raw ? raw.split(',') : [];
|
||||||
}
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -62,6 +94,7 @@ export default {
|
|||||||
<div class="col span-12">
|
<div class="col span-12">
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="namespaces"
|
v-model:value="namespaces"
|
||||||
|
:loading="loading"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
label-key="nameNsDescription.namespace.label"
|
label-key="nameNsDescription.namespace.label"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
<script>
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterUpgradeConfig',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
RadioGroup
|
||||||
|
},
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let parseDefaultValue = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
parseDefaultValue = this.value.value ? JSON.parse(this.value.value) : JSON.parse(this.value.default);
|
||||||
|
} catch (error) {
|
||||||
|
parseDefaultValue = JSON.parse(this.value.default);
|
||||||
|
}
|
||||||
|
parseDefaultValue = this.normalizeValue(parseDefaultValue);
|
||||||
|
|
||||||
|
return {
|
||||||
|
parseDefaultValue,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
strategyOptions() {
|
||||||
|
return [
|
||||||
|
{ value: 'sequential', label: 'sequential' },
|
||||||
|
{ value: 'skip', label: 'skip' },
|
||||||
|
{ value: 'parallel', label: 'parallel' }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
normalizeValue(obj) {
|
||||||
|
if (!obj.imagePreloadOption) {
|
||||||
|
obj.imagePreloadOption = { strategy: { type: 'sequential' } };
|
||||||
|
}
|
||||||
|
if (!obj.imagePreloadOption.strategy) {
|
||||||
|
obj.imagePreloadOption.strategy = { type: 'sequential' };
|
||||||
|
}
|
||||||
|
if (!obj.imagePreloadOption.strategy.type) {
|
||||||
|
obj.imagePreloadOption.strategy.type = 'sequential';
|
||||||
|
}
|
||||||
|
// Only set concurrency if type is 'parallel'
|
||||||
|
if (obj.imagePreloadOption.strategy.type === 'parallel') {
|
||||||
|
if (typeof obj.imagePreloadOption.strategy.concurrency !== 'number') {
|
||||||
|
obj.imagePreloadOption.strategy.concurrency = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete obj.imagePreloadOption.strategy.concurrency;
|
||||||
|
}
|
||||||
|
if (typeof obj.restoreVM !== 'boolean') {
|
||||||
|
obj.restoreVM = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
update() {
|
||||||
|
try {
|
||||||
|
// Clone to avoid mutating the form state
|
||||||
|
const valueToSave = JSON.parse(JSON.stringify(this.parseDefaultValue));
|
||||||
|
|
||||||
|
if (valueToSave.imagePreloadOption && valueToSave.imagePreloadOption.strategy) {
|
||||||
|
if (valueToSave.imagePreloadOption.strategy.type !== 'parallel') {
|
||||||
|
delete valueToSave.imagePreloadOption.strategy.concurrency;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value['value'] = JSON.stringify(valueToSave, null, 2);
|
||||||
|
this.errors = [];
|
||||||
|
} catch (e) {
|
||||||
|
this.errors = ['Invalid JSON'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler(neu) {
|
||||||
|
let parseDefaultValue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
parseDefaultValue = JSON.parse(neu.value);
|
||||||
|
} catch (err) {
|
||||||
|
parseDefaultValue = JSON.parse(this.value.default);
|
||||||
|
}
|
||||||
|
parseDefaultValue = this.normalizeValue(parseDefaultValue);
|
||||||
|
this['parseDefaultValue'] = parseDefaultValue;
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
@input="update"
|
||||||
|
>
|
||||||
|
<div class="col span-12">
|
||||||
|
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.imagePreloadStrategy') }}</b></label>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="parseDefaultValue.imagePreloadOption.strategy.type"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.setting.upgrade.strategyType')"
|
||||||
|
:options="strategyOptions"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-if="parseDefaultValue.imagePreloadOption.strategy.type === 'parallel'"
|
||||||
|
v-model:value.number="parseDefaultValue.imagePreloadOption.strategy.concurrency"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.setting.upgrade.concurrency')"
|
||||||
|
min="0"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.restoreVM') }}</b></label>
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="parseDefaultValue.restoreVM"
|
||||||
|
class="mb-20"
|
||||||
|
name="restoreVM"
|
||||||
|
:options="[true, false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="errors.length"
|
||||||
|
class="error"
|
||||||
|
>
|
||||||
|
{{ errors[0] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.error {
|
||||||
|
color: #d9534f;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
<script>
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import ArrayList from '@shell/components/form/ArrayList';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { isValidCIDR } from '@shell/utils/validators/cidr';
|
||||||
|
import { NODE } from '@shell/config/types';
|
||||||
|
import { _EDIT } from '@shell/config/query-params';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
|
||||||
|
const DEFAULT_NETWORK = {
|
||||||
|
clusterNetwork: '',
|
||||||
|
vlan: '',
|
||||||
|
range: '',
|
||||||
|
exclude: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VMMigrationNetwork',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
RadioGroup,
|
||||||
|
Banner,
|
||||||
|
ArrayList,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ value: '' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await allHash({
|
||||||
|
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
|
||||||
|
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
|
||||||
|
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fetchError = null;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch network data:', e); // eslint-disable-line no-console
|
||||||
|
this.fetchError = this.t('harvester.setting.vmMigrationNetwork.fetchError', { error: e.message || e }, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const { parsed, enabled, parseError } = this.parseInitialValue();
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
network: { ...DEFAULT_NETWORK, ...parsed },
|
||||||
|
fetchError: null,
|
||||||
|
parseError,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.registerBeforeHook?.(this.willSave, 'willSave');
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
allErrors() {
|
||||||
|
return [this.fetchError, this.parseError].filter(Boolean);
|
||||||
|
},
|
||||||
|
|
||||||
|
clusterNetworkOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const networks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||||
|
|
||||||
|
return networks.map((net) => ({
|
||||||
|
label: net.isReadyForStorageNetwork ? net.id : `${ net.id } (${ this.t('generic.notReady') })`,
|
||||||
|
value: net.id,
|
||||||
|
disabled: !net.isReadyForStorageNetwork,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
disableEdit() {
|
||||||
|
return !!(this.fetchError || this.parseError);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
parseInitialValue() {
|
||||||
|
let parsed = {};
|
||||||
|
let enabled = false;
|
||||||
|
let parseError = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof this.value.value === 'string' && this.value.value.trim()) {
|
||||||
|
parsed = JSON.parse(this.value.value);
|
||||||
|
enabled = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[VMMigrationNetwork] Failed to parse value:', e); // eslint-disable-line no-console
|
||||||
|
parseError = this.t('harvester.setting.vmMigrationNetwork.parseError', { error: e.message }, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(parsed.exclude)) {
|
||||||
|
parsed.exclude = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
parsed, enabled, parseError
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
clearErrors() {
|
||||||
|
this.fetchError = null;
|
||||||
|
this.parseError = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
inputVlan(val) {
|
||||||
|
this.network.vlan = val ? Math.min(4094, Math.max(1, Number(val))) : '';
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
this.network = { ...DEFAULT_NETWORK };
|
||||||
|
this.value.value = '';
|
||||||
|
this.enabled = false;
|
||||||
|
this.clearErrors();
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
try {
|
||||||
|
this.value.value = this.enabled ? JSON.stringify({
|
||||||
|
...this.network,
|
||||||
|
exclude: (this.network.exclude || []).filter((e) => !!e?.trim()),
|
||||||
|
}) : '';
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to stringify network config:', e); // eslint-disable-line no-console
|
||||||
|
this.value.value = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
validateInputs() {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (!this.network.clusterNetwork) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.clusterNetwork') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.network.range) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.range.label') }, true));
|
||||||
|
} else if (!isValidCIDR(this.network.range)) {
|
||||||
|
errors.push(this.t('harvester.setting.vmMigrationNetwork.range.invalid', null, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.network.vlan === '') {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.vlan') }, true));
|
||||||
|
} else {
|
||||||
|
const vlan = Number(this.network.vlan);
|
||||||
|
|
||||||
|
if (isNaN(vlan) || vlan < 1 || vlan > 4094) {
|
||||||
|
errors.push(this.t('validation.between', {
|
||||||
|
key: this.t('harvester.setting.vmMigrationNetwork.vlan'),
|
||||||
|
min: 1,
|
||||||
|
max: 4094,
|
||||||
|
}, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cidr of this.network.exclude || []) {
|
||||||
|
if (cidr && !isValidCIDR(cidr)) {
|
||||||
|
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', { value: cidr }, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
},
|
||||||
|
|
||||||
|
async willSave() {
|
||||||
|
if (!this.enabled) {
|
||||||
|
this.useDefault();
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
|
||||||
|
const errors = this.validateInputs();
|
||||||
|
|
||||||
|
return errors.length ? Promise.reject(errors) : Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Banner
|
||||||
|
v-for="(errorMsg, index) in allErrors"
|
||||||
|
:key="index"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{{ errorMsg }}
|
||||||
|
</Banner>
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="enabled"
|
||||||
|
class="mb-20"
|
||||||
|
name="enableMigrationNetwork"
|
||||||
|
:options="[true, false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
<template v-if="enabled">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="network.clusterNetwork"
|
||||||
|
required
|
||||||
|
label-key="harvester.setting.vmMigrationNetwork.clusterNetwork"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
:options="clusterNetworkOptions"
|
||||||
|
:disabled="disableEdit"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value.number="network.vlan"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
class="mb-20"
|
||||||
|
:min="1"
|
||||||
|
:max="4094"
|
||||||
|
:mode="mode"
|
||||||
|
placeholder="e.g. 1 - 4094"
|
||||||
|
label-key="harvester.setting.vmMigrationNetwork.vlan"
|
||||||
|
:disabled="disableEdit"
|
||||||
|
@update:value="inputVlan"
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="network.range"
|
||||||
|
required
|
||||||
|
class="mb-5"
|
||||||
|
:mode="mode"
|
||||||
|
:placeholder="t('harvester.setting.vmMigrationNetwork.range.placeholder')"
|
||||||
|
label-key="harvester.setting.vmMigrationNetwork.range.label"
|
||||||
|
:disabled="disableEdit"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
<ArrayList
|
||||||
|
v-model:value="network.exclude"
|
||||||
|
:show-header="true"
|
||||||
|
:default-add-value="''"
|
||||||
|
:mode="mode"
|
||||||
|
:add-disabled="disableEdit"
|
||||||
|
:add-label="t('harvester.setting.vmMigrationNetwork.exclude.addButton')"
|
||||||
|
:value-label="t('harvester.setting.vmMigrationNetwork.exclude.label')"
|
||||||
|
:value-placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
// suffix of doc link, see utils/feature-flags.js how to get complete doc link
|
// suffix of doc link, see utils/feature-flags.js how to get complete doc link
|
||||||
export const DOC = {
|
export const DOC = {
|
||||||
CONSOLE_URL: `/host/#remote-console`,
|
CONSOLE_URL: `/host/#remote-console`,
|
||||||
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
|
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
|
||||||
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
|
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
|
||||||
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
|
UPGRADE_URL: `/upgrade/index`,
|
||||||
UPGRADE_URL: `/upgrade/index`
|
UPGRADE_CONFIG_URL: `/advanced/index#upgrade-config`,
|
||||||
|
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
|
||||||
|
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const FEATURE_FLAGS = {
|
|||||||
'vmSnapshotQuota',
|
'vmSnapshotQuota',
|
||||||
'longhornV2LVMSupport',
|
'longhornV2LVMSupport',
|
||||||
'improveMaintenanceMode',
|
'improveMaintenanceMode',
|
||||||
|
'upgradeConfigSetting'
|
||||||
],
|
],
|
||||||
'v1.4.1': [],
|
'v1.4.1': [],
|
||||||
'v1.4.2': [
|
'v1.4.2': [
|
||||||
@@ -38,7 +39,13 @@ const FEATURE_FLAGS = {
|
|||||||
],
|
],
|
||||||
'v1.5.1': [],
|
'v1.5.1': [],
|
||||||
'v1.6.0': [
|
'v1.6.0': [
|
||||||
'vmMachineTypes'
|
'vmMachineTypes',
|
||||||
|
'customSupportBundle',
|
||||||
|
'csiOnlineExpandValidation',
|
||||||
|
'vmNetworkMigration',
|
||||||
|
'kubeovnVpcSubnet',
|
||||||
|
'rancherClusterSetting',
|
||||||
|
'cpuMemoryHotplug'
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -424,6 +424,7 @@ export function init($plugin, store) {
|
|||||||
[
|
[
|
||||||
HCI.CLUSTER_NETWORK,
|
HCI.CLUSTER_NETWORK,
|
||||||
HCI.NETWORK_ATTACHMENT,
|
HCI.NETWORK_ATTACHMENT,
|
||||||
|
HCI.VPC,
|
||||||
HCI.LB,
|
HCI.LB,
|
||||||
HCI.IP_POOL,
|
HCI.IP_POOL,
|
||||||
],
|
],
|
||||||
@@ -550,6 +551,21 @@ export function init($plugin, store) {
|
|||||||
exact: false
|
exact: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
configureType(HCI.VPC, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||||
|
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.vpc.label',
|
||||||
|
name: HCI.VPC,
|
||||||
|
namespaced: true,
|
||||||
|
weight: 187,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VPC }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.VPC,
|
||||||
|
});
|
||||||
|
|
||||||
configureType(HCI.SNAPSHOT, {
|
configureType(HCI.SNAPSHOT, {
|
||||||
isCreatable: false,
|
isCreatable: false,
|
||||||
location: {
|
location: {
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export const ADD_ONS = {
|
|||||||
RANCHER_MONITORING: 'rancher-monitoring',
|
RANCHER_MONITORING: 'rancher-monitoring',
|
||||||
VM_IMPORT_CONTROLLER: 'vm-import-controller',
|
VM_IMPORT_CONTROLLER: 'vm-import-controller',
|
||||||
LVM_DRIVER: 'lvm.driver.harvesterhci.io',
|
LVM_DRIVER: 'lvm.driver.harvesterhci.io',
|
||||||
|
KUBEOVN_OPERATOR: 'kubeovn-operator',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CSI_SECRETS = {
|
export const CSI_SECRETS = {
|
||||||
|
|||||||
@@ -69,5 +69,7 @@ export const HCI = {
|
|||||||
DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine',
|
DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine',
|
||||||
K8S_ARCH: 'kubernetes.io/arch',
|
K8S_ARCH: 'kubernetes.io/arch',
|
||||||
IMAGE_DISPLAY_NAME: 'harvesterhci.io/imageDisplayName',
|
IMAGE_DISPLAY_NAME: 'harvesterhci.io/imageDisplayName',
|
||||||
CUSTOM_IP: 'harvesterhci.io/custom-ip'
|
CUSTOM_IP: 'harvesterhci.io/custom-ip',
|
||||||
|
IMPORTED_IMAGE: 'migration.harvesterhci.io/imported',
|
||||||
|
VM_CPU_MEMORY_HOTPLUG: 'harvesterhci.io/enableCPUAndMemoryHotplug',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export const CLUSTER_NETWORK = 'clusterNetwork';
|
export const CLUSTER_NETWORK = 'clusterNetwork';
|
||||||
|
export const VPC = 'vpc';
|
||||||
|
|||||||
@@ -28,12 +28,17 @@ export const HCI_SETTING = {
|
|||||||
RELEASE_DOWNLOAD_URL: 'release-download-url',
|
RELEASE_DOWNLOAD_URL: 'release-download-url',
|
||||||
CCM_CSI_VERSION: 'harvester-csi-ccm-versions',
|
CCM_CSI_VERSION: 'harvester-csi-ccm-versions',
|
||||||
CSI_DRIVER_CONFIG: 'csi-driver-config',
|
CSI_DRIVER_CONFIG: 'csi-driver-config',
|
||||||
|
CSI_ONLINE_EXPAND_VALIDATION: 'csi-online-expand-validation',
|
||||||
VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds',
|
VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds',
|
||||||
NTP_SERVERS: 'ntp-servers',
|
NTP_SERVERS: 'ntp-servers',
|
||||||
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
|
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
|
||||||
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
|
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
|
||||||
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
|
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
|
||||||
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
|
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
|
||||||
|
UPGRADE_CONFIG: 'upgrade-config',
|
||||||
|
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
||||||
|
RANCHER_CLUSTER: 'rancher-cluster',
|
||||||
|
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_ALLOWED_SETTINGS = {
|
export const HCI_ALLOWED_SETTINGS = {
|
||||||
@@ -52,12 +57,15 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
from: 'import',
|
from: 'import',
|
||||||
featureFlag: 'autoRotateRke2CertsSetting'
|
featureFlag: 'autoRotateRke2CertsSetting'
|
||||||
},
|
},
|
||||||
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
|
[HCI_SETTING.CSI_ONLINE_EXPAND_VALIDATION]: {
|
||||||
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
|
kind: 'json', from: 'import', featureFlag: 'csiOnlineExpandValidation'
|
||||||
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
|
},
|
||||||
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
|
||||||
[HCI_SETTING.ADDITIONAL_CA]: {
|
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
|
||||||
|
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
|
||||||
|
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
|
||||||
|
[HCI_SETTING.ADDITIONAL_CA]: {
|
||||||
kind: 'multiline', canReset: true, from: 'import'
|
kind: 'multiline', canReset: true, from: 'import'
|
||||||
},
|
},
|
||||||
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
|
||||||
@@ -96,6 +104,16 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
featureFlag: 'longhornV2LVMSupport'
|
featureFlag: 'longhornV2LVMSupport'
|
||||||
},
|
},
|
||||||
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
|
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
|
||||||
|
[HCI_SETTING.UPGRADE_CONFIG]: {
|
||||||
|
kind: 'json',
|
||||||
|
from: 'import',
|
||||||
|
featureFlag: 'upgradeConfigSetting',
|
||||||
|
docPath: 'UPGRADE_CONFIG_URL'
|
||||||
|
},
|
||||||
|
[HCI_SETTING.RANCHER_CLUSTER]: {
|
||||||
|
kind: 'custom', from: 'import', canReset: true, featureFlag: 'rancherClusterSetting'
|
||||||
|
},
|
||||||
|
[HCI_SETTING.MAX_HOTPLUG_RATIO]: { kind: 'number', featureFlag: 'cpuMemoryHotplug' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
||||||
|
|||||||
@@ -104,3 +104,30 @@ export const HARVESTER_DESCRIPTION = {
|
|||||||
...DESCRIPTION,
|
...DESCRIPTION,
|
||||||
width: 150,
|
width: 150,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The CIDR_BLOCK column in VPC list page
|
||||||
|
export const CIDR_BLOCK = {
|
||||||
|
name: 'cidrBlock',
|
||||||
|
labelKey: 'harvester.subnet.cidrBlock.label',
|
||||||
|
sort: 'cidrBlock',
|
||||||
|
value: 'spec.cidrBlock',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// The Protocol column in VPC list page
|
||||||
|
export const PROTOCOL = {
|
||||||
|
name: 'protocol',
|
||||||
|
labelKey: 'harvester.subnet.protocol.label',
|
||||||
|
sort: 'protocol',
|
||||||
|
value: 'spec.protocol',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// The Provider column in VPC list page
|
||||||
|
export const PROVIDER = {
|
||||||
|
name: 'provider',
|
||||||
|
labelKey: 'harvester.subnet.provider.label',
|
||||||
|
sort: 'provider',
|
||||||
|
value: 'spec.provider',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,10 +5,21 @@ export const BACKUP_TYPE = {
|
|||||||
|
|
||||||
export const NETWORK_TYPE = {
|
export const NETWORK_TYPE = {
|
||||||
L2VLAN: 'L2VlanNetwork',
|
L2VLAN: 'L2VlanNetwork',
|
||||||
UNTAGGED: 'UntaggedNetwork'
|
UNTAGGED: 'UntaggedNetwork',
|
||||||
|
OVERLAY: 'OverlayNetwork',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VOLUME_MODE = {
|
export const VOLUME_MODE = {
|
||||||
BLOCK: 'Block',
|
BLOCK: 'Block',
|
||||||
FILE_SYSTEM: 'Filesystem'
|
FILE_SYSTEM: 'Filesystem'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const NETWORK_PROTOCOL = {
|
||||||
|
IPv4: 'IPv4',
|
||||||
|
IPv6: 'IPv6',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const INTERNAL_STORAGE_CLASS = {
|
||||||
|
VMSTATE_PERSISTENCE: 'vmstate-persistence',
|
||||||
|
LONGHORN_STATIC: 'longhorn-static',
|
||||||
|
};
|
||||||
|
|||||||
@@ -172,6 +172,9 @@ export default {
|
|||||||
:cpu="cpu"
|
:cpu="cpu"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:memory="memory"
|
:memory="memory"
|
||||||
|
:max-cpu="maxCpu"
|
||||||
|
:max-memory="maxMemory"
|
||||||
|
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
@@ -172,6 +172,9 @@ export default {
|
|||||||
:cpu="cpu"
|
:cpu="cpu"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:memory="memory"
|
:memory="memory"
|
||||||
|
:max-cpu="maxCpu"
|
||||||
|
:max-memory="maxMemory"
|
||||||
|
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="row mb-10">
|
<div class="row mb-10">
|
||||||
|
|||||||
+3
-2
@@ -5,6 +5,7 @@ import CreateEditView from '@shell/mixins/create-edit-view';
|
|||||||
import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress';
|
import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress';
|
||||||
import VMConsoleBar from '../../../components/VMConsoleBar';
|
import VMConsoleBar from '../../../components/VMConsoleBar';
|
||||||
import { HCI } from '../../../types';
|
import { HCI } from '../../../types';
|
||||||
|
import { getVmCPUMemoryValues } from '../../../utils/cpuMemory';
|
||||||
|
|
||||||
const UNDEFINED = 'n/a';
|
const UNDEFINED = 'n/a';
|
||||||
|
|
||||||
@@ -91,9 +92,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
flavor() {
|
flavor() {
|
||||||
const domain = this.value?.spec?.template?.spec?.domain;
|
const { cpu, memory } = getVmCPUMemoryValues(this.value);
|
||||||
|
|
||||||
return `${ domain.cpu?.cores } vCPU , ${ domain.resources?.limits?.memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
|
return `${ cpu } vCPU , ${ memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
|
||||||
},
|
},
|
||||||
|
|
||||||
kernelRelease() {
|
kernelRelease() {
|
||||||
|
|||||||
@@ -139,10 +139,18 @@ export default {
|
|||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
const vmimList = this.$store.getters[`${ inStore }/all`](HCI.VMIM) || [];
|
const vmimList = this.$store.getters[`${ inStore }/all`](HCI.VMIM) || [];
|
||||||
|
|
||||||
const migrationUid = this.vmi?.status?.migrationState?.migrationUid;
|
const vmiName = this.vmi?.name || '';
|
||||||
const vmim = vmimList.find((VMIM) => VMIM?.metadata?.uid === migrationUid);
|
|
||||||
|
|
||||||
return vmim;
|
// filter the corresponding vmims by vmim.spec.vmiName and find the latest one by creationTimestamp
|
||||||
|
const vmim = vmimList.filter((VMIM) => VMIM?.spec?.vmiName === vmiName).sort((a, b) => {
|
||||||
|
if (a?.metadata?.creationTimestamp > b?.metadata?.creationTimestamp) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return vmim.length > 0 && vmim[0] ? vmim[0] : null;
|
||||||
},
|
},
|
||||||
|
|
||||||
migrationEvents() {
|
migrationEvents() {
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
<script>
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { getVmCPUMemoryValues } from '../utils/cpuMemory';
|
||||||
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { GIBIBYTE } from '../utils/unit';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CPUMemoryHotplugModal',
|
||||||
|
|
||||||
|
emits: ['close'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton, Card, Banner, UnitInput
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const { cpu, memory } = getVmCPUMemoryValues(this.resources[0] || {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
cpu,
|
||||||
|
memory,
|
||||||
|
errors: [],
|
||||||
|
GIBIBYTE
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0] || {};
|
||||||
|
},
|
||||||
|
maxResourcesMessage() {
|
||||||
|
const { maxCpu, maxMemory } = getVmCPUMemoryValues(this.actionResource);
|
||||||
|
|
||||||
|
if (maxCpu && maxMemory) {
|
||||||
|
return this.t('harvester.modal.cpuMemoryHotplug.maxResourcesMessage', { maxCpu, maxMemory });
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.cpu = '';
|
||||||
|
this.memory = '';
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
change() {
|
||||||
|
if (parseInt(this.memory, 10) < 1 ) {
|
||||||
|
this.memory = '1Gi';
|
||||||
|
}
|
||||||
|
if (this.cpu < 1) {
|
||||||
|
this.cpu = 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(buttonCb) {
|
||||||
|
if (this.actionResource) {
|
||||||
|
try {
|
||||||
|
const res = await this.actionResource.doAction('cpuAndMemoryHotplug', { sockets: this.cpu, memory: this.memory });
|
||||||
|
|
||||||
|
if (res._status === 200 || res._status === 204) {
|
||||||
|
this.$store.dispatch('growl/success', {
|
||||||
|
title: this.t('generic.notification.title.succeed'),
|
||||||
|
message: this.t('harvester.modal.cpuMemoryHotplug.success', { vm: this.actionResource.nameDisplay })
|
||||||
|
}, { root: true });
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
buttonCb(true);
|
||||||
|
} else {
|
||||||
|
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||||
|
|
||||||
|
this['errors'] = error;
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const error = err?.data || err;
|
||||||
|
const message = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
|
this['errors'] = message;
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card
|
||||||
|
ref="modal"
|
||||||
|
name="modal"
|
||||||
|
:show-highlight-border="false"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<h4
|
||||||
|
v-clean-html="t('harvester.modal.cpuMemoryHotplug.title')"
|
||||||
|
class="text-default-text"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<Banner
|
||||||
|
v-if="maxResourcesMessage"
|
||||||
|
:label="maxResourcesMessage"
|
||||||
|
color="info"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="cpu"
|
||||||
|
:label="t('harvester.virtualMachine.input.cpu')"
|
||||||
|
:delay="0"
|
||||||
|
required
|
||||||
|
suffix="C"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
:min="1"
|
||||||
|
@update:value="change"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="memory"
|
||||||
|
:label="t('harvester.virtualMachine.input.memory')"
|
||||||
|
:mode="mode"
|
||||||
|
:input-exponent="3"
|
||||||
|
:delay="0"
|
||||||
|
:min="1"
|
||||||
|
:increment="1024"
|
||||||
|
:output-modifier="true"
|
||||||
|
:disabled="disabled"
|
||||||
|
:suffix="GIBIBYTE"
|
||||||
|
class="mb-20"
|
||||||
|
@update:value="change"
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
:label="err"
|
||||||
|
color="error"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<div class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn role-secondary mr-10"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
<AsyncButton
|
||||||
|
mode="apply"
|
||||||
|
@click="save"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -10,6 +10,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
|||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { NAMESPACE, STORAGE_CLASS } from '@shell/config/types';
|
import { NAMESPACE, STORAGE_CLASS } from '@shell/config/types';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { isInternalStorageClass } from '../utils/storage-class';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterExportImageDialog',
|
name: 'HarvesterExportImageDialog',
|
||||||
@@ -105,11 +106,17 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const options = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
|
const options = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||||
|
const isInternal = isInternalStorageClass(s.metadata?.name);
|
||||||
|
|
||||||
|
if (isInternal) {
|
||||||
|
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
value: s.name,
|
value: s.name,
|
||||||
|
disabled: isInternal,
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { NAMESPACE } from '@shell/config/types';
|
||||||
import { randomStr } from '@shell/utils/string';
|
import { randomStr } from '@shell/utils/string';
|
||||||
import { exceptionToErrorsArray, stringify } from '@shell/utils/error';
|
import { exceptionToErrorsArray, stringify } from '@shell/utils/error';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
@@ -6,7 +7,15 @@ import AsyncButton from '@shell/components/AsyncButton';
|
|||||||
import GraphCircle from '@shell/components/graph/Circle';
|
import GraphCircle from '@shell/components/graph/Circle';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import AppModal from '@shell/components/AppModal';
|
import AppModal from '@shell/components/AppModal';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
|
import { HCI_SETTING } from '../config/settings';
|
||||||
|
import { DOC } from '../config/doc-links';
|
||||||
|
import { docLink } from '../utils/feature-flags';
|
||||||
|
|
||||||
|
const SELECT_ALL = 'select_all';
|
||||||
|
const UNSELECT_ALL = 'unselect_all';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SupportBundle',
|
name: 'SupportBundle',
|
||||||
@@ -17,14 +26,36 @@ export default {
|
|||||||
AsyncButton,
|
AsyncButton,
|
||||||
Banner,
|
Banner,
|
||||||
AppModal,
|
AppModal,
|
||||||
|
LabeledSelect,
|
||||||
|
UnitInput
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
||||||
|
const response = await this.$store.dispatch('harvester/request', { url });
|
||||||
|
|
||||||
|
this.defaultNamespaces = response.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
this.defaultNamespaces = [];
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
url: '',
|
isOpen: false,
|
||||||
description: '',
|
errors: [],
|
||||||
errors: [],
|
version: '',
|
||||||
isOpen: false,
|
clusterName: '',
|
||||||
|
url: '',
|
||||||
|
description: '',
|
||||||
|
namespaces: [],
|
||||||
|
defaultNamespaces: [],
|
||||||
|
timeout: '',
|
||||||
|
expiration: '',
|
||||||
|
nodeTimeout: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -39,23 +70,51 @@ export default {
|
|||||||
|
|
||||||
percentage() {
|
percentage() {
|
||||||
return this.$store.getters['harvester-common/getBundlePercentage'];
|
return this.$store.getters['harvester-common/getBundlePercentage'];
|
||||||
}
|
},
|
||||||
|
|
||||||
|
availableNamespaces() {
|
||||||
|
const allNamespaces = this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
||||||
|
const defaultNamespacesIds = this.defaultNamespaces.map((ns) => ns.id);
|
||||||
|
|
||||||
|
return allNamespaces.filter((ns) => !defaultNamespacesIds.includes(ns) || this.namespaces.includes(ns));
|
||||||
|
},
|
||||||
|
|
||||||
|
namespaceOptions() {
|
||||||
|
if (this.availableNamespaces.length === 0) return [];
|
||||||
|
|
||||||
|
const allSelected = this.namespaces.length === this.availableNamespaces.length &&
|
||||||
|
this.availableNamespaces.every((ns) => this.namespaces.includes(ns));
|
||||||
|
|
||||||
|
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
|
||||||
|
|
||||||
|
return [controlOption, ...this.availableNamespaces];
|
||||||
|
},
|
||||||
|
|
||||||
|
docLink() {
|
||||||
|
const version = this.$store.getters['harvester-common/getServerVersion']();
|
||||||
|
|
||||||
|
return docLink(DOC.SUPPORT_BUNDLE_NAMESPACES, version);
|
||||||
|
},
|
||||||
|
|
||||||
|
customSupportBundleFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
isShowBundleModal: {
|
isShowBundleModal: {
|
||||||
|
immediate: true,
|
||||||
handler(show) {
|
handler(show) {
|
||||||
if (show) {
|
this.isOpen = show;
|
||||||
this.$nextTick(() => {
|
}
|
||||||
this.isOpen = true;
|
},
|
||||||
});
|
|
||||||
} else {
|
isOpen(newVal) {
|
||||||
this.isOpen = false;
|
if (newVal) {
|
||||||
this.url = '';
|
this.loadDefaultSettings();
|
||||||
this.description = '';
|
} else {
|
||||||
}
|
this.resetForm();
|
||||||
},
|
}
|
||||||
immediate: true
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -65,33 +124,93 @@ export default {
|
|||||||
close() {
|
close() {
|
||||||
this.isOpen = false;
|
this.isOpen = false;
|
||||||
this.$store.commit('harvester-common/toggleBundleModal', false);
|
this.$store.commit('harvester-common/toggleBundleModal', false);
|
||||||
this.backUpName = '';
|
},
|
||||||
|
|
||||||
|
loadDefaultSettings() {
|
||||||
|
const cluster = this.$store.getters['currentCluster'];
|
||||||
|
const versionSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SERVER_VERSION);
|
||||||
|
const namespacesSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES);
|
||||||
|
const timeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT);
|
||||||
|
const expirationSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION);
|
||||||
|
const nodeTimeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT);
|
||||||
|
|
||||||
|
this.version = versionSetting?.currentVersion || '';
|
||||||
|
this.clusterName = cluster?.id || '';
|
||||||
|
this.namespaces = (namespacesSetting?.value ?? namespacesSetting?.default ?? '').split(',').map((ns) => ns.trim()).filter((ns) => ns);
|
||||||
|
this.timeout = timeoutSetting?.value ?? timeoutSetting?.default ?? '';
|
||||||
|
this.expiration = expirationSetting?.value ?? expirationSetting?.default ?? '';
|
||||||
|
this.nodeTimeout = nodeTimeoutSetting?.value ?? nodeTimeoutSetting?.default ?? '';
|
||||||
|
this.url = '';
|
||||||
|
this.description = '';
|
||||||
|
this.errors = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
resetForm() {
|
||||||
|
this.url = '';
|
||||||
|
this.description = '';
|
||||||
|
this.namespaces = [];
|
||||||
|
this.timeout = '';
|
||||||
|
this.expiration = '';
|
||||||
|
this.nodeTimeout = '';
|
||||||
|
this.errors = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
updateNamespaces(selected) {
|
||||||
|
if (selected.includes(SELECT_ALL)) {
|
||||||
|
this.namespaces = [...this.availableNamespaces];
|
||||||
|
} else if (selected.includes(UNSELECT_ALL)) {
|
||||||
|
this.namespaces = [];
|
||||||
|
} else {
|
||||||
|
this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateNumberValue(field, value) {
|
||||||
|
if (value === '' || value === null || isNaN(value)) {
|
||||||
|
this[field] = '';
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(value);
|
||||||
|
const isValid = Number.isInteger(num) && num >= 0;
|
||||||
|
|
||||||
|
this[field] = isValid ? String(num) : '';
|
||||||
|
},
|
||||||
|
|
||||||
|
onKeyDown(e) {
|
||||||
|
if (['e', 'E', '+', '-', '.'].includes(e.key)) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async save(buttonCb) {
|
async save(buttonCb) {
|
||||||
this.errors = [];
|
this.errors = [];
|
||||||
|
|
||||||
const name = `bundle-${ randomStr(5).toLowerCase() }`;
|
const name = `bundle-${ this.clusterName }-${ this.version }-${ randomStr(5).toLowerCase() }`;
|
||||||
const namespace = 'harvester-system';
|
const namespace = 'harvester-system';
|
||||||
|
|
||||||
|
const spec = {
|
||||||
|
description: this.description.trim(),
|
||||||
|
...(this.url.trim() && { issueURL: this.url.trim() }),
|
||||||
|
...(this.namespaces.length > 0 && { extraCollectionNamespaces: this.namespaces }),
|
||||||
|
...(this.timeout !== '' && { timeout: Number(this.timeout) }),
|
||||||
|
...(this.expiration !== '' && { expiration: Number(this.expiration) }),
|
||||||
|
...(this.nodeTimeout !== '' && { nodeTimeout: Number(this.nodeTimeout) }),
|
||||||
|
};
|
||||||
|
|
||||||
const bundleCrd = {
|
const bundleCrd = {
|
||||||
apiVersion: 'harvesterhci.io/v1beta1',
|
apiVersion: 'harvesterhci.io/v1beta1',
|
||||||
type: HCI.SUPPORT_BUNDLE,
|
type: HCI.SUPPORT_BUNDLE,
|
||||||
kind: 'SupportBundle',
|
kind: 'SupportBundle',
|
||||||
metadata: {
|
metadata: { name, namespace },
|
||||||
name,
|
spec,
|
||||||
namespace
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
issueURL: this.url,
|
|
||||||
description: this.description
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
|
||||||
|
|
||||||
await bundleValue.save();
|
await bundleValue.save();
|
||||||
|
|
||||||
this.$store.commit('harvester-common/setLatestBundleId', `${ namespace }/${ name }`, { root: true });
|
this.$store.commit('harvester-common/setLatestBundleId', `${ namespace }/${ name }`, { root: true });
|
||||||
@@ -118,69 +237,110 @@ export default {
|
|||||||
@close="close"
|
@close="close"
|
||||||
>
|
>
|
||||||
<div class="p-20">
|
<div class="p-20">
|
||||||
<h2>
|
<h2>{{ t('harvester.modal.bundle.title') }}</h2>
|
||||||
{{ t('harvester.modal.bundle.title') }}
|
<div class="content">
|
||||||
</h2>
|
<div
|
||||||
|
v-if="bundlePending"
|
||||||
<div
|
class="circle mb-20"
|
||||||
v-if="!bundlePending"
|
>
|
||||||
class="content"
|
|
||||||
>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="url"
|
|
||||||
:label="t('harvester.modal.bundle.url')"
|
|
||||||
class="mb-20"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="description"
|
|
||||||
:label="t('harvester.modal.bundle.description')"
|
|
||||||
type="multiline"
|
|
||||||
:min-height="120"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="content"
|
|
||||||
>
|
|
||||||
<div class="circle">
|
|
||||||
<GraphCircle
|
<GraphCircle
|
||||||
primary-stroke-color="green"
|
primary-stroke-color="green"
|
||||||
secondary-stroke-color="white"
|
secondary-stroke-color="lightgrey"
|
||||||
:stroke-width="6"
|
:stroke-width="6"
|
||||||
:percentage="percentage"
|
:percentage="percentage"
|
||||||
:show-text="true"
|
:show-text="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<template v-else>
|
||||||
|
<p
|
||||||
|
v-if="customSupportBundleFeatureEnabled"
|
||||||
|
v-clean-html="t('harvester.modal.bundle.tip', { doc: docLink }, true)"
|
||||||
|
class="mb-20"
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="url"
|
||||||
|
:label="t('harvester.modal.bundle.url')"
|
||||||
|
class="mb-10"
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="description"
|
||||||
|
required
|
||||||
|
:label="t('harvester.modal.bundle.description')"
|
||||||
|
type="multiline"
|
||||||
|
:min-height="80"
|
||||||
|
class="mb-10"
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<template v-if="customSupportBundleFeatureEnabled">
|
||||||
v-for="(err, idx) in errors"
|
<LabeledSelect
|
||||||
:key="idx"
|
v-model:value="namespaces"
|
||||||
>
|
:label="t('harvester.modal.bundle.namespaces.label')"
|
||||||
<Banner
|
:clearable="true"
|
||||||
color="error"
|
:multiple="true"
|
||||||
:label="stringify(err)"
|
:options="namespaceOptions"
|
||||||
/>
|
class="mb-10 label-select"
|
||||||
</div>
|
:tooltip="t('harvester.modal.bundle.namespaces.tooltip', _, true)"
|
||||||
|
@update:value="updateNamespaces"
|
||||||
<div class="footer mt-20">
|
/>
|
||||||
<button
|
<UnitInput
|
||||||
class="btn btn-sm role-secondary mr-10"
|
v-model:value="timeout"
|
||||||
@click="close"
|
:label="t('harvester.modal.bundle.timeout.label')"
|
||||||
|
class="mb-10"
|
||||||
|
type="number"
|
||||||
|
:min="0"
|
||||||
|
:tooltip="t('harvester.modal.bundle.timeout.tooltip', _, true)"
|
||||||
|
:suffix="timeout > 1 ? 'Minutes' : 'Minute'"
|
||||||
|
@keydown="onKeyDown"
|
||||||
|
@update:value="val => updateNumberValue('timeout', val)"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="expiration"
|
||||||
|
:label="t('harvester.modal.bundle.expiration.label')"
|
||||||
|
class="mb-10"
|
||||||
|
type="number"
|
||||||
|
:min="0"
|
||||||
|
:tooltip="t('harvester.modal.bundle.expiration.tooltip', _, true)"
|
||||||
|
:suffix="expiration > 1 ? 'Minutes' : 'Minute'"
|
||||||
|
@keydown="onKeyDown"
|
||||||
|
@update:value="val => updateNumberValue('expiration', val)"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="nodeTimeout"
|
||||||
|
:label="t('harvester.modal.bundle.nodeTimeout.label')"
|
||||||
|
class="mb-10"
|
||||||
|
type="number"
|
||||||
|
:min="0"
|
||||||
|
:tooltip="t('harvester.modal.bundle.nodeTimeout.tooltip', _, true)"
|
||||||
|
:suffix="nodeTimeout > 1 ? 'Minutes' : 'Minute'"
|
||||||
|
@keydown="onKeyDown"
|
||||||
|
@update:value="val => updateNumberValue('nodeTimeout', val)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-for="(err, idx) in errors"
|
||||||
|
:key="idx"
|
||||||
>
|
>
|
||||||
{{ t('generic.close') }}
|
<Banner
|
||||||
</button>
|
color="error"
|
||||||
|
:label="stringify(err)"
|
||||||
<AsyncButton
|
/>
|
||||||
type="submit"
|
</div>
|
||||||
mode="generate"
|
<div class="footer mt-20">
|
||||||
class="btn btn-sm bg-primary"
|
<button
|
||||||
:disabled="bundlePending"
|
class="btn btn-sm role-secondary mr-10"
|
||||||
@click="save"
|
@click="close"
|
||||||
/>
|
>
|
||||||
|
{{ t('generic.close') }}
|
||||||
|
</button>
|
||||||
|
<AsyncButton
|
||||||
|
type="submit"
|
||||||
|
mode="generate"
|
||||||
|
class="btn btn-sm bg-primary"
|
||||||
|
:disabled="bundlePending"
|
||||||
|
@click="save"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</app-modal>
|
</app-modal>
|
||||||
@@ -194,6 +354,10 @@ export default {
|
|||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.labeled-select.taggable ::v-deep(.vs__selected-options .vs__selected.vs__selected > button) {
|
||||||
|
margin: 0 7px;
|
||||||
|
}
|
||||||
|
|
||||||
.bundle {
|
.bundle {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
@@ -204,10 +368,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
height: 218px;
|
|
||||||
|
|
||||||
.circle {
|
.circle {
|
||||||
padding-top: 20px;
|
padding: 10px 0;
|
||||||
height: 160px;
|
height: 160px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,8 +73,10 @@ export default {
|
|||||||
|
|
||||||
data.spec.clusterNetwork = this.clusterNetwork;
|
data.spec.clusterNetwork = this.clusterNetwork;
|
||||||
|
|
||||||
|
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl'](`v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`);
|
||||||
|
|
||||||
await this.$store.dispatch('harvester/request', {
|
await this.$store.dispatch('harvester/request', {
|
||||||
url: `/v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`,
|
url,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
|||||||
import { RadioGroup } from '@components/Form/Radio';
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import { NETWORK_TYPE } from '../config/types';
|
import { NETWORK_TYPE } from '../config/types';
|
||||||
|
|
||||||
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
|
const { L2VLAN, UNTAGGED, OVERLAY } = NETWORK_TYPE;
|
||||||
|
|
||||||
const AUTO = 'auto';
|
const AUTO = 'auto';
|
||||||
const MANUAL = 'manual';
|
const MANUAL = 'manual';
|
||||||
@@ -44,13 +43,10 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
const config = JSON.parse(this.value.spec.config);
|
const config = JSON.parse(this.value.spec.config);
|
||||||
|
|
||||||
const annotations = this.value?.metadata?.annotations || {};
|
const annotations = this.value?.metadata?.annotations || {};
|
||||||
const layer3Network = JSON.parse(annotations[HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE] || '{}');
|
const layer3Network = JSON.parse(annotations[HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE] || '{}');
|
||||||
|
|
||||||
if ((config.bridge || '').endsWith('-br')) {
|
|
||||||
config.bridge = config.bridge.slice(0, -3);
|
|
||||||
}
|
|
||||||
|
|
||||||
const type = this.value.vlanType || L2VLAN ;
|
const type = this.value.vlanType || L2VLAN ;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -78,6 +74,30 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
clusterBridge: {
|
||||||
|
get() {
|
||||||
|
if (!this.config.bridge) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove -br suffix if exists
|
||||||
|
return this.config?.bridge?.endsWith('-br') ? this.config.bridge.slice(0, -3) : '';
|
||||||
|
},
|
||||||
|
|
||||||
|
set(neu) {
|
||||||
|
if (neu === '') {
|
||||||
|
this.config.bridge = '';
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!neu.endsWith('-br')) {
|
||||||
|
this.config.bridge = `${ neu }-br`;
|
||||||
|
} else {
|
||||||
|
this.config.bridge = neu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
modeOptions() {
|
modeOptions() {
|
||||||
return [{
|
return [{
|
||||||
label: this.t('harvester.network.layer3Network.mode.auto'),
|
label: this.t('harvester.network.layer3Network.mode.auto'),
|
||||||
@@ -88,6 +108,14 @@ export default {
|
|||||||
}];
|
}];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
kubeovnVpcSubnetSupport() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('kubeovnVpcSubnet');
|
||||||
|
},
|
||||||
|
|
||||||
|
longhornV2LVMSupport() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('longhornV2LVMSupport');
|
||||||
|
},
|
||||||
|
|
||||||
clusterNetworkOptions() {
|
clusterNetworkOptions() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||||
@@ -103,8 +131,30 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
networkType() {
|
networkTypes() {
|
||||||
return [L2VLAN, UNTAGGED];
|
const types = [L2VLAN, UNTAGGED];
|
||||||
|
|
||||||
|
if (this.kubeovnVpcSubnetSupport) {
|
||||||
|
types.push(OVERLAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return types;
|
||||||
|
},
|
||||||
|
|
||||||
|
isL2VlanNetwork() {
|
||||||
|
if (this.isView) {
|
||||||
|
return this.value.vlanType === L2VLAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.type === L2VLAN;
|
||||||
|
},
|
||||||
|
|
||||||
|
isOverlayNetwork() {
|
||||||
|
if (this.isView) {
|
||||||
|
return this.value.vlanType === OVERLAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.type === OVERLAY;
|
||||||
},
|
},
|
||||||
|
|
||||||
isUntaggedNetwork() {
|
isUntaggedNetwork() {
|
||||||
@@ -116,36 +166,54 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
type(newType) {
|
||||||
|
if (newType === OVERLAY) {
|
||||||
|
this.config.type = 'kube-ovn';
|
||||||
|
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
|
||||||
|
this.config.server_socket = '/run/openvswitch/kube-ovn-daemon.sock';
|
||||||
|
} else {
|
||||||
|
this.config.type = 'bridge';
|
||||||
|
this.config.promiscMode = true;
|
||||||
|
this.config.ipam = {};
|
||||||
|
this.config.bridge = '';
|
||||||
|
delete this.config.provider;
|
||||||
|
delete this.config.server_socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async saveNetwork(buttonCb) {
|
async saveNetwork(buttonCb) {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
if (!this.config.vlan && !this.isUntaggedNetwork) {
|
if (this.isL2VlanNetwork || this.isUntaggedNetwork) {
|
||||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
|
if (!this.config.vlan && !this.isUntaggedNetwork) {
|
||||||
}
|
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
|
||||||
|
|
||||||
if (!this.config.bridge) {
|
|
||||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') }));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.layer3Network.mode === MANUAL) {
|
|
||||||
if (!this.layer3Network.gateway) {
|
|
||||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') }));
|
|
||||||
}
|
}
|
||||||
if (!this.layer3Network.cidr) {
|
|
||||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') }));
|
if (!this.config.bridge) {
|
||||||
|
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.layer3Network.mode === MANUAL) {
|
||||||
|
if (!this.layer3Network.gateway) {
|
||||||
|
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') }));
|
||||||
|
}
|
||||||
|
if (!this.layer3Network.cidr) {
|
||||||
|
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
buttonCb(false);
|
||||||
|
this.errors = errors;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
buttonCb(false);
|
|
||||||
this.errors = errors;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
|
|
||||||
|
|
||||||
await this.save(buttonCb);
|
await this.save(buttonCb);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -169,14 +237,19 @@ export default {
|
|||||||
updateBeforeSave() {
|
updateBeforeSave() {
|
||||||
this.config.name = this.value.metadata.name;
|
this.config.name = this.value.metadata.name;
|
||||||
|
|
||||||
|
if (this.isOverlayNetwork) {
|
||||||
|
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
|
||||||
|
delete this.config.bridge;
|
||||||
|
delete this.config.promiscMode;
|
||||||
|
delete this.config.vlan;
|
||||||
|
delete this.config.ipam;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isUntaggedNetwork) {
|
if (this.isUntaggedNetwork) {
|
||||||
delete this.config.vlan;
|
delete this.config.vlan;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.value.spec.config = JSON.stringify({
|
this.value.spec.config = JSON.stringify({ ...this.config });
|
||||||
...this.config,
|
|
||||||
bridge: `${ this.config.bridge }-br`,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -212,14 +285,15 @@ export default {
|
|||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="type"
|
v-model:value="type"
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
:options="networkType"
|
:options="networkTypes"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
|
:disabled="isEdit"
|
||||||
:label="t('harvester.fields.type')"
|
:label="t('harvester.fields.type')"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-if="!isUntaggedNetwork"
|
v-if="isL2VlanNetwork"
|
||||||
v-model:value.number="config.vlan"
|
v-model:value.number="config.vlan"
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
required
|
required
|
||||||
@@ -229,25 +303,20 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
@update:value="input"
|
@update:value="input"
|
||||||
/>
|
/>
|
||||||
|
<LabeledSelect
|
||||||
<div class="row">
|
v-if="!isOverlayNetwork"
|
||||||
<div
|
v-model:value="clusterBridge"
|
||||||
class="col span-12"
|
class="mb-20"
|
||||||
>
|
:label="t('harvester.network.clusterNetwork.label')"
|
||||||
<LabeledSelect
|
required
|
||||||
v-model:value="config.bridge"
|
:disabled="isEdit"
|
||||||
class="mb-20"
|
:options="clusterNetworkOptions"
|
||||||
:label="t('harvester.network.clusterNetwork.label')"
|
:mode="mode"
|
||||||
required
|
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
|
||||||
:options="clusterNetworkOptions"
|
/>
|
||||||
:mode="mode"
|
|
||||||
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
v-if="!isUntaggedNetwork"
|
v-if="isL2VlanNetwork"
|
||||||
name="layer3Network"
|
name="layer3Network"
|
||||||
:label="t('harvester.network.tabs.layer3Network')"
|
:label="t('harvester.network.tabs.layer3Network')"
|
||||||
:weight="98"
|
:weight="98"
|
||||||
|
|||||||
@@ -84,6 +84,10 @@ export default {
|
|||||||
this.value['_type'] = TYPES.OPAQUE;
|
this.value['_type'] = TYPES.OPAQUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( this.realMode === _CLONE) {
|
||||||
|
this.value['_type'] = this.liveValue._type;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isCloud,
|
isCloud,
|
||||||
nodeDrivers: null,
|
nodeDrivers: null,
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|||||||
import { TextAreaAutoGrow } from '@components/Form/TextArea';
|
import { TextAreaAutoGrow } from '@components/Form/TextArea';
|
||||||
import UnitInput from '@shell/components/form/UnitInput';
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { DOC } from '../config/doc-links';
|
||||||
|
import { docLink } from '../utils/feature-flags';
|
||||||
|
|
||||||
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
|
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
|
||||||
|
|
||||||
@@ -58,7 +60,13 @@ export default {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
setting,
|
setting,
|
||||||
description: isHarvester ? t(`advancedSettings.descriptions.harv-${ this.value.id }`) : t(`advancedSettings.descriptions.${ this.value.id }`),
|
description: isHarvester ? t(
|
||||||
|
`advancedSettings.descriptions.harv-${ this.value.id }`,
|
||||||
|
this.getDocLinkParams()
|
||||||
|
) : t(
|
||||||
|
`advancedSettings.descriptions.${ this.value.id }`,
|
||||||
|
this.getDocLinkParams()
|
||||||
|
),
|
||||||
editHelp: t(`advancedSettings.editHelp.${ this.value.id }`),
|
editHelp: t(`advancedSettings.editHelp.${ this.value.id }`),
|
||||||
enumOptions,
|
enumOptions,
|
||||||
canReset,
|
canReset,
|
||||||
@@ -171,6 +179,18 @@ export default {
|
|||||||
if (typeof this.$refs.settingComp?.useDefault === 'function') {
|
if (typeof this.$refs.settingComp?.useDefault === 'function') {
|
||||||
this.$refs.settingComp.useDefault();
|
this.$refs.settingComp.useDefault();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
getDocLinkParams() {
|
||||||
|
const setting = HCI_ALLOWED_SETTINGS[this.value.id] || HCI_SINGLE_CLUSTER_ALLOWED_SETTING[this.value.id];
|
||||||
|
|
||||||
|
if (setting?.docPath) {
|
||||||
|
const version = this.$store.getters['harvester-common/getServerVersion']();
|
||||||
|
const url = docLink(DOC[setting.docPath], version);
|
||||||
|
|
||||||
|
return { url };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { VM_IMAGE_FILE_FORMAT } from '../validators/vm-image';
|
|||||||
import { OS } from '../mixins/harvester-vm';
|
import { OS } from '../mixins/harvester-vm';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
||||||
|
import { isInternalStorageClass } from '../utils/storage-class';
|
||||||
|
|
||||||
const ENCRYPT = 'encrypt';
|
const ENCRYPT = 'encrypt';
|
||||||
const DECRYPT = 'decrypt';
|
const DECRYPT = 'decrypt';
|
||||||
@@ -168,11 +169,18 @@ export default {
|
|||||||
|
|
||||||
return filteredStorages
|
return filteredStorages
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||||
|
let disabled = false;
|
||||||
|
|
||||||
|
if (isInternalStorageClass(s.name)) {
|
||||||
|
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
||||||
|
disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
value: s.name,
|
value: s.name,
|
||||||
|
disabled
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -259,6 +259,9 @@ export default {
|
|||||||
<CpuMemory
|
<CpuMemory
|
||||||
:cpu="cpu"
|
:cpu="cpu"
|
||||||
:memory="memory"
|
:memory="memory"
|
||||||
|
:max-cpu="maxCpu"
|
||||||
|
:max-memory="maxMemory"
|
||||||
|
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
||||||
:disabled="isConfig"
|
:disabled="isConfig"
|
||||||
@updateCpuMemory="updateCpuMemory"
|
@updateCpuMemory="updateCpuMemory"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
|||||||
import { DATA_ENGINE_V2 } from '../models/harvester/persistentvolumeclaim';
|
import { DATA_ENGINE_V2 } from '../models/harvester/persistentvolumeclaim';
|
||||||
import { GIBIBYTE } from '../utils/unit';
|
import { GIBIBYTE } from '../utils/unit';
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||||
|
import { isInternalStorageClass } from '../utils/storage-class';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterVolume',
|
name: 'HarvesterVolume',
|
||||||
@@ -206,11 +207,18 @@ export default {
|
|||||||
|
|
||||||
storageClassOptions() {
|
storageClassOptions() {
|
||||||
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||||
|
let disabled = false;
|
||||||
|
|
||||||
|
if (isInternalStorageClass(s.name)) {
|
||||||
|
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
||||||
|
disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
value: s.name,
|
value: s.name,
|
||||||
|
disabled
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
},
|
},
|
||||||
@@ -304,6 +312,10 @@ export default {
|
|||||||
return ['ReadWriteMany'];
|
return ['ReadWriteMany'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.value?.spec?.accessModes && this.value?.spec?.accessModes?.length > 0) {
|
||||||
|
return this.value.spec.accessModes;
|
||||||
|
}
|
||||||
|
|
||||||
const storageClassName = this.value.spec.storageClassName;
|
const storageClassName = this.value.spec.storageClassName;
|
||||||
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
|
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,323 @@
|
|||||||
|
<script>
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import { NETWORK_PROTOCOL, NETWORK_TYPE } from '@pkg/harvester/config/types';
|
||||||
|
import { set } from '@shell/utils/object';
|
||||||
|
import ArrayList from '@shell/components/form/ArrayList';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import ResourceTabs from '@shell/components/form/ResourceTabs/index';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditSubnet',
|
||||||
|
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
CruResource,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
NameNsDescription,
|
||||||
|
Tab,
|
||||||
|
RadioGroup,
|
||||||
|
ArrayList,
|
||||||
|
ResourceTabs,
|
||||||
|
Loading,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
created() {
|
||||||
|
const vpc = this.$route.query.vpc || '';
|
||||||
|
|
||||||
|
set(this.value, 'spec', this.value.spec || {
|
||||||
|
cidrBlock: '',
|
||||||
|
protocol: NETWORK_PROTOCOL.IPv4,
|
||||||
|
provider: '',
|
||||||
|
vpc,
|
||||||
|
gatewayIP: '',
|
||||||
|
excludeIps: [],
|
||||||
|
private: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const hash = {
|
||||||
|
vpc: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
|
||||||
|
nad: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }),
|
||||||
|
};
|
||||||
|
|
||||||
|
await allHash(hash);
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
showAllowSubnets() {
|
||||||
|
return this.value?.spec?.private === true;
|
||||||
|
},
|
||||||
|
|
||||||
|
doneLocationOverride() {
|
||||||
|
return this.value.doneOverride;
|
||||||
|
},
|
||||||
|
|
||||||
|
allowSubnetTooltip() {
|
||||||
|
return this.t('harvester.subnet.allowSubnet.tooltip', null, true);
|
||||||
|
},
|
||||||
|
|
||||||
|
excludeIPsTooltip() {
|
||||||
|
return this.t('harvester.subnet.excludeIPs.tooltip', null, true);
|
||||||
|
},
|
||||||
|
|
||||||
|
protocolOptions() {
|
||||||
|
return Object.values(NETWORK_PROTOCOL);
|
||||||
|
},
|
||||||
|
|
||||||
|
provider: {
|
||||||
|
get() {
|
||||||
|
const raw = this.value.spec.provider;
|
||||||
|
|
||||||
|
if (!raw) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const vmNet = raw.split('.')[0] || '';
|
||||||
|
const ns = raw.split('.')[1] || '';
|
||||||
|
|
||||||
|
return `${ ns }/${ vmNet }`;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
const ns = value.split('/')[0] || '';
|
||||||
|
const vmNet = value.split('/')[1] || '';
|
||||||
|
const provider = `${ vmNet }.${ ns }.ovn`;
|
||||||
|
|
||||||
|
set(this.value, 'spec.provider', provider);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
providerOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const vmNets = this.$store.getters[`${ inStore }/all`](NETWORK_ATTACHMENT) || [];
|
||||||
|
|
||||||
|
return vmNets.filter((net) => net.vlanType === NETWORK_TYPE.OVERLAY).map((n) => ({
|
||||||
|
label: n.id,
|
||||||
|
value: n.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
vpcOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const vpcs = this.$store.getters[`${ inStore }/all`](HCI.VPC) || [];
|
||||||
|
|
||||||
|
return vpcs.map((n) => ({
|
||||||
|
label: n.id,
|
||||||
|
value: n.id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async saveSubnet(buttonCb) {
|
||||||
|
const errors = [];
|
||||||
|
const name = this.value?.metadata?.name;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!name) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('generic.name') }, true));
|
||||||
|
} else if (!this.value?.spec?.cidrBlock) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.cidrBlock.label') }, true));
|
||||||
|
} else if (!this.value?.spec?.provider) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.provider.label') }, true));
|
||||||
|
} else if (this.value.spec.excludeIps.includes('')) {
|
||||||
|
errors.push(this.t('harvester.validation.subnet.excludeIps'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
buttonCb(false);
|
||||||
|
this.errors = errors;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await this.value.save();
|
||||||
|
buttonCb(true);
|
||||||
|
this.done();
|
||||||
|
} catch (e) {
|
||||||
|
this.errors = [e];
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<CruResource
|
||||||
|
v-else
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
:errors="errors"
|
||||||
|
@finish="saveSubnet"
|
||||||
|
@error="e=>errors=e"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:namespaced="false"
|
||||||
|
@update:value="$emit('update:value', $event)"
|
||||||
|
/>
|
||||||
|
<ResourceTabs
|
||||||
|
class="mt-15"
|
||||||
|
:need-events="false"
|
||||||
|
:need-related="false"
|
||||||
|
:mode="mode"
|
||||||
|
:side-tabs="true"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="Basic"
|
||||||
|
:label="t('generic.basic')"
|
||||||
|
:weight="-1"
|
||||||
|
class="bordered-table"
|
||||||
|
>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="value.spec.cidrBlock"
|
||||||
|
class="mb-20"
|
||||||
|
required
|
||||||
|
:placeholder="t('harvester.subnet.cidrBlock.placeholder')"
|
||||||
|
:label="t('harvester.subnet.cidrBlock.label')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="value.spec.protocol"
|
||||||
|
:label="t('harvester.subnet.protocol.label')"
|
||||||
|
:options="protocolOptions"
|
||||||
|
required
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="provider"
|
||||||
|
:label="t('harvester.subnet.provider.label')"
|
||||||
|
:options="providerOptions"
|
||||||
|
:tooltip="t('harvester.subnet.provider.tooltip')"
|
||||||
|
required
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="value.spec.vpc"
|
||||||
|
:label="t('harvester.subnet.vpc.label')"
|
||||||
|
:options="vpcOptions"
|
||||||
|
required
|
||||||
|
:disabled="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="value.spec.gateway"
|
||||||
|
class="mb-20"
|
||||||
|
:placeholder="t('harvester.subnet.gateway.placeholder')"
|
||||||
|
:label="t('harvester.subnet.gateway.label')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="value.spec.private"
|
||||||
|
name="enabled"
|
||||||
|
:options="[true, false]"
|
||||||
|
:label="t('harvester.subnet.private.label')"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
:mode="mode"
|
||||||
|
tooltip-key="harvester.subnet.private.tooltip"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ArrayList
|
||||||
|
v-if="showAllowSubnets"
|
||||||
|
v-model:value="value.spec.allowSubnets"
|
||||||
|
:show-header="true"
|
||||||
|
class="mt-20"
|
||||||
|
:mode="mode"
|
||||||
|
:add-label="t('harvester.subnet.allowSubnet.addSubnet')"
|
||||||
|
>
|
||||||
|
<template #column-headers>
|
||||||
|
<div class="box">
|
||||||
|
<h3 class="key">
|
||||||
|
{{ t('harvester.subnet.allowSubnet.label') }}
|
||||||
|
<i
|
||||||
|
v-clean-tooltip="{content: allowSubnetTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||||
|
v-stripped-aria-label="allowSubnetTooltip"
|
||||||
|
class="icon icon-info"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #columns="scope">
|
||||||
|
<div class="key">
|
||||||
|
<input
|
||||||
|
v-model="scope.row.value"
|
||||||
|
:placeholder="t('harvester.subnet.allowSubnet.placeholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ArrayList>
|
||||||
|
<ArrayList
|
||||||
|
v-model:value="value.spec.excludeIps"
|
||||||
|
:show-header="true"
|
||||||
|
class="mt-20"
|
||||||
|
:mode="mode"
|
||||||
|
:add-label="t('harvester.setting.storageNetwork.exclude.addIp')"
|
||||||
|
>
|
||||||
|
<template #column-headers>
|
||||||
|
<div class="box">
|
||||||
|
<h3 class="key">
|
||||||
|
{{ t('harvester.setting.storageNetwork.exclude.label') }}
|
||||||
|
<i
|
||||||
|
v-clean-tooltip="{content: excludeIPsTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||||
|
v-stripped-aria-label="excludeIPsTooltip"
|
||||||
|
class="icon icon-info"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #columns="scope">
|
||||||
|
<div class="key">
|
||||||
|
<input
|
||||||
|
v-model="scope.row.value"
|
||||||
|
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ArrayList>
|
||||||
|
</Tab>
|
||||||
|
</ResourceTabs>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
<script>
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
||||||
|
import { removeAt } from '@shell/utils/array';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'StaticRoutes',
|
||||||
|
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const rows = (this.value || []).map((row) => {
|
||||||
|
return {
|
||||||
|
cidr: row.cidr || '',
|
||||||
|
nextHopIP: row.nextHopIP || '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { rows };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isView() {
|
||||||
|
return this.mode === _VIEW;
|
||||||
|
},
|
||||||
|
|
||||||
|
showAdd() {
|
||||||
|
return !this.isView;
|
||||||
|
},
|
||||||
|
|
||||||
|
showRemove() {
|
||||||
|
return !this.isView;
|
||||||
|
},
|
||||||
|
nextHopIPTooltip() {
|
||||||
|
return this.t('harvester.vpc.staticRoutes.nextHopIP.tooltip');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.queueUpdate = debounce(this.update, 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
add() {
|
||||||
|
this.rows.push({
|
||||||
|
cidr: '',
|
||||||
|
nextHopIP: '',
|
||||||
|
});
|
||||||
|
this.queueUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(idx) {
|
||||||
|
removeAt(this.rows, idx);
|
||||||
|
this.queueUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (this.isView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('update:value', this.rows);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="rows.length"
|
||||||
|
class="static-route-row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(row, idx) in rows"
|
||||||
|
:key="idx"
|
||||||
|
>
|
||||||
|
<div class="pool-headers cidr">
|
||||||
|
<span class="pool-cidr">
|
||||||
|
<t k="harvester.vpc.staticRoutes.cidr.label" />
|
||||||
|
</span>
|
||||||
|
<span class="pool-nextHopIP">
|
||||||
|
<t k="harvester.vpc.staticRoutes.nextHopIP.label" />
|
||||||
|
<i
|
||||||
|
v-clean-tooltip="{content: nextHopIPTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||||
|
v-stripped-aria-label="nextHopIPTooltip"
|
||||||
|
class="icon icon-info"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="pool-row cidr">
|
||||||
|
<div class="pool-cidr">
|
||||||
|
<span v-if="isView">
|
||||||
|
{{ row.cidr }}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="row.cidr"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('harvester.vpc.staticRoutes.cidr.placeholder')"
|
||||||
|
@input="queueUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="pool-nextHopIP">
|
||||||
|
<span v-if="isView">
|
||||||
|
{{ row.nextHopIP }}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="row.nextHopIP"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('harvester.vpc.staticRoutes.nextHopIP.placeholder')"
|
||||||
|
@input="queueUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="showRemove"
|
||||||
|
type="button"
|
||||||
|
class="btn role-link pl-0"
|
||||||
|
@click="remove(idx)"
|
||||||
|
>
|
||||||
|
<t k="generic.remove" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="showAdd"
|
||||||
|
type="button"
|
||||||
|
class="btn role-tertiary add"
|
||||||
|
@click="add()"
|
||||||
|
>
|
||||||
|
<t k="generic.add" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.pool-headers, .pool-row {
|
||||||
|
display: grid;
|
||||||
|
grid-column-gap: $column-gutter;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&.cidr {
|
||||||
|
grid-template-columns: 40%+$column-gutter 40%+$column-gutter 15%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
<script>
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
||||||
|
import { removeAt } from '@shell/utils/array';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VpcPeerings',
|
||||||
|
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: { LabeledSelect },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
vpc: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
await this.$store.dispatch('harvester/findAll', { type: HCI.VPC });
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const rows = (this.value || []).map((row) => {
|
||||||
|
return {
|
||||||
|
localConnectIP: row.localConnectIP || '',
|
||||||
|
remoteVpc: row.remoteVpc || '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { rows };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isView() {
|
||||||
|
return this.mode === _VIEW;
|
||||||
|
},
|
||||||
|
|
||||||
|
isEdit() {
|
||||||
|
return this.mode === _EDIT;
|
||||||
|
},
|
||||||
|
|
||||||
|
showAdd() {
|
||||||
|
return !this.isView;
|
||||||
|
},
|
||||||
|
|
||||||
|
showRemove() {
|
||||||
|
return !this.isView;
|
||||||
|
},
|
||||||
|
|
||||||
|
remoteVpcOptions() {
|
||||||
|
const allVpcs = this.$store.getters['harvester/all'](HCI.VPC) || [];
|
||||||
|
|
||||||
|
// filter self vpc if editing
|
||||||
|
const vpcs = this.isEdit ? allVpcs.filter((v) => v.id !== this.vpc.id) : allVpcs;
|
||||||
|
|
||||||
|
return vpcs.map((n) => ({
|
||||||
|
label: n.id,
|
||||||
|
value: n.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
localConnectIPTooltip() {
|
||||||
|
return this.t('harvester.vpc.vpcPeerings.localConnectIP.tooltip');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.queueUpdate = debounce(this.update, 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
add() {
|
||||||
|
this.rows.push({
|
||||||
|
localConnectIP: '',
|
||||||
|
remoteVpc: '',
|
||||||
|
});
|
||||||
|
this.queueUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(idx) {
|
||||||
|
removeAt(this.rows, idx);
|
||||||
|
this.queueUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (this.isView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$emit('update:value', this.rows);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="rows.length"
|
||||||
|
class="static-route-row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(row, idx) in rows"
|
||||||
|
:key="idx"
|
||||||
|
>
|
||||||
|
<div class="pool-headers localConnectIP">
|
||||||
|
<span class="pool-localConnectIP">
|
||||||
|
<t k="harvester.vpc.vpcPeerings.localConnectIP.label" />
|
||||||
|
<i
|
||||||
|
v-clean-tooltip="{content: localConnectIPTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||||
|
v-stripped-aria-label="localConnectPTooltip"
|
||||||
|
class="icon icon-info"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span class="pool-remoteVpc">
|
||||||
|
<t k="harvester.vpc.vpcPeerings.remoteVpc.label" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="pool-row localConnectIP">
|
||||||
|
<div class="pool-localConnectIP">
|
||||||
|
<span v-if="isView">
|
||||||
|
{{ row.localConnectIP }}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="row.localConnectIP"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('harvester.vpc.vpcPeerings.localConnectIP.placeholder')"
|
||||||
|
@input="queueUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="pool-remoteVpc">
|
||||||
|
<span v-if="isView">
|
||||||
|
{{ row.remoteVpc }}
|
||||||
|
</span>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="row.remoteVpc"
|
||||||
|
:options="remoteVpcOptions"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="queueUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="showRemove"
|
||||||
|
type="button"
|
||||||
|
class="btn role-link pl-0"
|
||||||
|
@click="remove(idx)"
|
||||||
|
>
|
||||||
|
<t k="generic.remove" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="showAdd"
|
||||||
|
type="button"
|
||||||
|
class="btn role-tertiary add"
|
||||||
|
@click="add()"
|
||||||
|
>
|
||||||
|
<t k="generic.add" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.pool-headers, .pool-row {
|
||||||
|
display: grid;
|
||||||
|
grid-column-gap: $column-gutter;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&.localConnectIP {
|
||||||
|
grid-template-columns: 40%+$column-gutter 40%+$column-gutter 15%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
<script>
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import ResourceTabs from '@shell/components/form/ResourceTabs/index';
|
||||||
|
import StaticRoutes from './StaticRoutes';
|
||||||
|
import VpcPeerings from './VpcPeerings';
|
||||||
|
import { set } from '@shell/utils/object';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditVPC',
|
||||||
|
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
CruResource,
|
||||||
|
NameNsDescription,
|
||||||
|
Tab,
|
||||||
|
StaticRoutes,
|
||||||
|
ResourceTabs,
|
||||||
|
Loading,
|
||||||
|
VpcPeerings
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
data() {
|
||||||
|
set(this.value, 'spec', this.value.spec || {
|
||||||
|
staticRoutes: [],
|
||||||
|
vpcPeerings: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return { staticRoutes: [] };
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async saveVpc(buttonCb) {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const name = this.value?.metadata?.name;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('generic.name') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
buttonCb(false);
|
||||||
|
this.errors = errors;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await this.value.save();
|
||||||
|
buttonCb(true);
|
||||||
|
this.done();
|
||||||
|
} catch (e) {
|
||||||
|
this.errors = [e];
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<CruResource
|
||||||
|
v-else
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
:errors="errors"
|
||||||
|
@finish="saveVpc"
|
||||||
|
@error="e=>errors=e"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:namespaced="false"
|
||||||
|
@update:value="$emit('update:value', $event)"
|
||||||
|
/>
|
||||||
|
<ResourceTabs
|
||||||
|
class="mt-15"
|
||||||
|
:mode="mode"
|
||||||
|
:side-tabs="true"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="staticRoutes"
|
||||||
|
:label="t('harvester.vpc.staticRoutes.label')"
|
||||||
|
:weight="-1"
|
||||||
|
class="bordered-table"
|
||||||
|
>
|
||||||
|
<StaticRoutes
|
||||||
|
v-model:value="value.spec.staticRoutes"
|
||||||
|
class="col span-12"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
name="vpcPeerings"
|
||||||
|
:label="t('harvester.vpc.vpcPeerings.label')"
|
||||||
|
:weight="-2"
|
||||||
|
class="bordered-table"
|
||||||
|
>
|
||||||
|
<VpcPeerings
|
||||||
|
v-model:value="value.spec.vpcPeerings"
|
||||||
|
class="col span-12"
|
||||||
|
:mode="mode"
|
||||||
|
:vpc="value"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</ResourceTabs>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
@@ -2,23 +2,44 @@
|
|||||||
import UnitInput from '@shell/components/form/UnitInput';
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||||
import { GIBIBYTE } from '../../utils/unit';
|
import { GIBIBYTE } from '../../utils/unit';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import { _VIEW } from '@shell/config/query-params';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { HCI_SETTING } from '../../config/settings';
|
||||||
|
|
||||||
|
const DEFAULT_HOT_PLUG_TIMES = 4;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditCpuMemory',
|
name: 'HarvesterEditCpuMemory',
|
||||||
|
|
||||||
emits: ['updateCpuMemory'],
|
emits: ['updateCpuMemory'],
|
||||||
|
|
||||||
components: { UnitInput, InputOrDisplay },
|
components: {
|
||||||
|
UnitInput, InputOrDisplay, Checkbox
|
||||||
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
cpu: {
|
cpu: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
maxCpu: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
memory: {
|
memory: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
maxMemory: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
enableHotPlug: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'create',
|
default: 'create',
|
||||||
@@ -29,21 +50,59 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const res = await allHash({ settings: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }) });
|
||||||
|
|
||||||
|
this.settings = res.settings || [];
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
GIBIBYTE,
|
GIBIBYTE,
|
||||||
localCpu: this.cpu,
|
localCpu: this.cpu,
|
||||||
localMemory: this.memory
|
localMemory: this.memory,
|
||||||
|
maxLocalCpu: this.maxCpu,
|
||||||
|
maxLocalMemory: this.maxMemory,
|
||||||
|
localEnableHotPlug: this.enableHotPlug,
|
||||||
|
settings: []
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
cupDisplay() {
|
isView() {
|
||||||
|
return this.mode === _VIEW;
|
||||||
|
},
|
||||||
|
cpuDisplay() {
|
||||||
return `${ this.localCpu } C`;
|
return `${ this.localCpu } C`;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
maxCpuDisplay() {
|
||||||
|
return `${ this.maxLocalCpu } C`;
|
||||||
|
},
|
||||||
|
|
||||||
memoryDisplay() {
|
memoryDisplay() {
|
||||||
return `${ this.localMemory }`;
|
return `${ this.localMemory }`;
|
||||||
|
},
|
||||||
|
|
||||||
|
maxMemoryDisplay() {
|
||||||
|
return `${ this.maxLocalMemory }`;
|
||||||
|
},
|
||||||
|
|
||||||
|
cpuMemoryHotplugTooltip() {
|
||||||
|
return this.t('harvester.virtualMachine.hotplug.tooltip', { hotPlugTimes: this.maxHotplugRatio });
|
||||||
|
},
|
||||||
|
|
||||||
|
isCPUMemoryHotPlugFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('cpuMemoryHotplug');
|
||||||
|
},
|
||||||
|
|
||||||
|
maxHotplugRatio() {
|
||||||
|
const maxHotPlugRatioSetting = this.settings.find((s) => s.id === HCI_SETTING.MAX_HOTPLUG_RATIO);
|
||||||
|
const maxPlugRatio = maxHotPlugRatioSetting?.value ? parseInt(maxHotPlugRatioSetting.value, 10) : DEFAULT_HOT_PLUG_TIMES;
|
||||||
|
|
||||||
|
return maxPlugRatio;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -55,71 +114,161 @@ export default {
|
|||||||
if (neu && !neu.includes('null')) {
|
if (neu && !neu.includes('null')) {
|
||||||
this.localMemory = neu;
|
this.localMemory = neu;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
maxCpu(neu) {
|
||||||
|
this.maxLocalCpu = neu;
|
||||||
|
},
|
||||||
|
maxMemory(neu) {
|
||||||
|
if (neu && !neu.includes('null')) {
|
||||||
|
this.maxLocalMemory = neu;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enableHotPlug(neu) {
|
||||||
|
this.localEnableHotPlug = neu;
|
||||||
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
change() {
|
hotPlugChanged(neu) {
|
||||||
let memory = '';
|
// If hot plug is enabled, we need to update the maxCpu and maxMemory values
|
||||||
|
if (neu) {
|
||||||
if (String(this.localMemory).includes('Gi')) {
|
this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null;
|
||||||
memory = this.localMemory;
|
this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null;
|
||||||
|
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, neu);
|
||||||
} else {
|
} else {
|
||||||
memory = `${ this.localMemory }${ GIBIBYTE }`;
|
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, '', null, neu);
|
||||||
}
|
}
|
||||||
if (memory.includes('null')) {
|
|
||||||
memory = null;
|
|
||||||
}
|
|
||||||
this.$emit('updateCpuMemory', this.localCpu, memory);
|
|
||||||
},
|
},
|
||||||
|
changeMemory() {
|
||||||
|
if (this.localEnableHotPlug) {
|
||||||
|
this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null;
|
||||||
|
}
|
||||||
|
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
|
||||||
|
},
|
||||||
|
changeCPU() {
|
||||||
|
if (this.localEnableHotPlug) {
|
||||||
|
this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null;
|
||||||
|
}
|
||||||
|
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
|
||||||
|
},
|
||||||
|
changeMaxCPUMemory() {
|
||||||
|
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="row">
|
<div>
|
||||||
<div class="col span-6 mb-10">
|
<div class="row">
|
||||||
<InputOrDisplay
|
<div class="col span-6 mb-10">
|
||||||
name="CPU"
|
<InputOrDisplay
|
||||||
:value="cupDisplay"
|
name="CPU"
|
||||||
:mode="mode"
|
:value="cpuDisplay"
|
||||||
>
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="localCpu"
|
|
||||||
label="CPU"
|
|
||||||
suffix="C"
|
|
||||||
:delay="0"
|
|
||||||
required
|
|
||||||
:disabled="disabled"
|
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
class="mb-20"
|
class="mb-10"
|
||||||
@update:value="change"
|
>
|
||||||
/>
|
<UnitInput
|
||||||
</InputOrDisplay>
|
v-model:value="localCpu"
|
||||||
|
label="CPU"
|
||||||
|
suffix="C"
|
||||||
|
:delay="0"
|
||||||
|
required
|
||||||
|
:disabled="disabled"
|
||||||
|
:mode="mode"
|
||||||
|
class="mb-20"
|
||||||
|
@update:value="changeCPU"
|
||||||
|
/>
|
||||||
|
</InputOrDisplay>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6 mb-10">
|
||||||
|
<InputOrDisplay
|
||||||
|
:name="t('harvester.virtualMachine.input.memory')"
|
||||||
|
:value="memoryDisplay"
|
||||||
|
:mode="mode"
|
||||||
|
>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="localMemory"
|
||||||
|
:label="t('harvester.virtualMachine.input.memory')"
|
||||||
|
:mode="mode"
|
||||||
|
:input-exponent="3"
|
||||||
|
:delay="0"
|
||||||
|
:increment="1024"
|
||||||
|
:output-modifier="true"
|
||||||
|
:disabled="disabled"
|
||||||
|
required
|
||||||
|
:suffix="GIBIBYTE"
|
||||||
|
class="mb-20"
|
||||||
|
@update:value="changeMemory"
|
||||||
|
/>
|
||||||
|
</InputOrDisplay>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
<div class="col span-6 mb-10">
|
v-if="isCPUMemoryHotPlugFeatureEnabled"
|
||||||
<InputOrDisplay
|
class="row"
|
||||||
:name="t('harvester.virtualMachine.input.memory')"
|
>
|
||||||
:value="memoryDisplay"
|
<Checkbox
|
||||||
:mode="mode"
|
v-model:value="localEnableHotPlug"
|
||||||
>
|
class="check"
|
||||||
<UnitInput
|
type="checkbox"
|
||||||
v-model:value="localMemory"
|
:label="t('harvester.virtualMachine.hotplug.title')"
|
||||||
:label="t('harvester.virtualMachine.input.memory')"
|
:disabled="isView"
|
||||||
|
@update:value="hotPlugChanged"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-clean-tooltip="{content: cpuMemoryHotplugTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||||
|
v-stripped-aria-label="cpuMemoryHotplugTooltip"
|
||||||
|
class="icon icon-info"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="localEnableHotPlug && isCPUMemoryHotPlugFeatureEnabled"
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div class="col span-6 mb-10">
|
||||||
|
<InputOrDisplay
|
||||||
|
:name="t('harvester.virtualMachine.input.maxCpu')"
|
||||||
|
:value="maxCpuDisplay"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:input-exponent="3"
|
class="mt-20"
|
||||||
:delay="0"
|
>
|
||||||
:increment="1024"
|
<UnitInput
|
||||||
:output-modifier="true"
|
v-model:value="maxLocalCpu"
|
||||||
:disabled="disabled"
|
:label="t('harvester.virtualMachine.input.maxCpu')"
|
||||||
required
|
suffix="C"
|
||||||
:suffix="GIBIBYTE"
|
:delay="0"
|
||||||
class="mb-20"
|
:disabled="disabled"
|
||||||
@update:value="change"
|
:mode="mode"
|
||||||
/>
|
class="mt-20"
|
||||||
</InputOrDisplay>
|
@update:value="changeMaxCPUMemory"
|
||||||
|
/>
|
||||||
|
</InputOrDisplay>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6 mb-10">
|
||||||
|
<InputOrDisplay
|
||||||
|
:name="t('harvester.virtualMachine.input.maxMemory')"
|
||||||
|
:value="maxMemoryDisplay"
|
||||||
|
:mode="mode"
|
||||||
|
class="mt-20"
|
||||||
|
>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="maxLocalMemory"
|
||||||
|
:label="t('harvester.virtualMachine.input.maxMemory')"
|
||||||
|
:mode="mode"
|
||||||
|
:input-exponent="3"
|
||||||
|
:delay="0"
|
||||||
|
:increment="1024"
|
||||||
|
:output-modifier="true"
|
||||||
|
:disabled="disabled"
|
||||||
|
:suffix="GIBIBYTE"
|
||||||
|
class="mt-20"
|
||||||
|
@update:value="changeMaxCPUMemory"
|
||||||
|
/>
|
||||||
|
</InputOrDisplay>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="mt-20">
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="checkedSsh"
|
v-model:value="checkedSsh"
|
||||||
:label="t('harvester.virtualMachine.input.sshKey')"
|
:label="t('harvester.virtualMachine.input.sshKey')"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { SOURCE_TYPE } from '../../../config/harvester-map';
|
|||||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
||||||
import { HCI } from '../../../types';
|
import { HCI } from '../../../types';
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||||
|
import { OFF } from '../../../models/kubevirt.io.virtualmachine';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ['update:value'],
|
emits: ['update:value'],
|
||||||
@@ -266,7 +267,17 @@ export default {
|
|||||||
|
|
||||||
isLonghornV2(volume) {
|
isLonghornV2(volume) {
|
||||||
return volume?.pvc?.isLonghornV2 || volume?.pvc?.storageClass?.isLonghornV2;
|
return volume?.pvc?.isLonghornV2 || volume?.pvc?.storageClass?.isLonghornV2;
|
||||||
}
|
},
|
||||||
|
|
||||||
|
isResizeDisabled(volume) {
|
||||||
|
if (this.isCreate) return false;
|
||||||
|
if (volume.newCreateId) return false;
|
||||||
|
|
||||||
|
const isStopped = this.vm.stateDisplay === OFF;
|
||||||
|
const isLonghornV2 = this.isLonghornV2(volume);
|
||||||
|
|
||||||
|
return !isStopped || isLonghornV2;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -347,6 +358,7 @@ export default {
|
|||||||
:is-edit="isEdit"
|
:is-edit="isEdit"
|
||||||
:is-view="isView"
|
:is-view="isView"
|
||||||
:is-virtual-type="isVirtualType"
|
:is-virtual-type="isVirtualType"
|
||||||
|
:is-resize-disabled="isResizeDisabled(rows[i])"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:idx="i"
|
:idx="i"
|
||||||
:validate-required="validateRequired"
|
:validate-required="validateRequired"
|
||||||
|
|||||||
+10
-8
@@ -64,7 +64,12 @@ export default {
|
|||||||
isVirtualType: {
|
isVirtualType: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
}
|
},
|
||||||
|
|
||||||
|
isResizeDisabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -105,8 +110,9 @@ export default {
|
|||||||
})
|
})
|
||||||
.sort((a, b) => (a.creationTimestamp > b.creationTimestamp ? -1 : 1))
|
.sort((a, b) => (a.creationTimestamp > b.creationTimestamp ? -1 : 1))
|
||||||
.map((image) => ({
|
.map((image) => ({
|
||||||
label: this.imageOptionLabel(image),
|
label: this.imageOptionLabel(image),
|
||||||
value: image.id,
|
value: image.id,
|
||||||
|
disabled: image.isImportedImage
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -134,10 +140,6 @@ export default {
|
|||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
return this.$store.getters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
||||||
},
|
},
|
||||||
|
|
||||||
isLonghornV2() {
|
|
||||||
return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.isLonghornV2;
|
|
||||||
},
|
|
||||||
|
|
||||||
selectedImage() {
|
selectedImage() {
|
||||||
return this.$store.getters['harvester/all'](HCI.IMAGE)?.find( (I) => this.value.image === I.id);
|
return this.$store.getters['harvester/all'](HCI.IMAGE)?.find( (I) => this.value.image === I.id);
|
||||||
},
|
},
|
||||||
@@ -349,7 +351,7 @@ export default {
|
|||||||
:label="t('harvester.fields.size')"
|
:label="t('harvester.fields.size')"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:required="validateRequired"
|
:required="validateRequired"
|
||||||
:disable="isLonghornV2"
|
:disabled="isResizeDisabled"
|
||||||
:suffix="GIBIBYTE"
|
:suffix="GIBIBYTE"
|
||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { ucFirst } from '@shell/utils/string';
|
|||||||
import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storageclass';
|
import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storageclass';
|
||||||
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
|
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
|
||||||
import { GIBIBYTE } from '../../../../utils/unit';
|
import { GIBIBYTE } from '../../../../utils/unit';
|
||||||
|
import { isInternalStorageClass } from '../../../../utils/storage-class';
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -55,7 +56,12 @@ export default {
|
|||||||
isVirtualType: {
|
isVirtualType: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
}
|
},
|
||||||
|
|
||||||
|
isResizeDisabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -118,19 +124,22 @@ export default {
|
|||||||
|
|
||||||
storageClassOptions() {
|
storageClassOptions() {
|
||||||
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||||
|
let disabled = false;
|
||||||
|
|
||||||
|
if (isInternalStorageClass(s.name)) {
|
||||||
|
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
||||||
|
disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
value: s.name,
|
value: s.name,
|
||||||
|
disabled
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
},
|
},
|
||||||
|
|
||||||
isLonghornV2() {
|
|
||||||
return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.isLonghornV2;
|
|
||||||
},
|
|
||||||
|
|
||||||
isLonghornStorageClass() {
|
isLonghornStorageClass() {
|
||||||
const selectedSC = this.storageClasses.find((sc) => sc.name === this.value?.storageClassName) || {};
|
const selectedSC = this.storageClasses.find((sc) => sc.name === this.value?.storageClassName) || {};
|
||||||
|
|
||||||
@@ -302,7 +311,7 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
:required="validateRequired"
|
:required="validateRequired"
|
||||||
:label="t('harvester.fields.size')"
|
:label="t('harvester.fields.size')"
|
||||||
:disabled="isLonghornV2"
|
:disabled="isResizeDisabled"
|
||||||
:suffix="GIBIBYTE"
|
:suffix="GIBIBYTE"
|
||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -237,6 +237,9 @@ export default {
|
|||||||
}
|
}
|
||||||
const versions = await this.$store.dispatch('harvester/findAll', { type: HCI.VM_VERSION });
|
const versions = await this.$store.dispatch('harvester/findAll', { type: HCI.VM_VERSION });
|
||||||
const curVersion = versions.find( (V) => V.id === id);
|
const curVersion = versions.find( (V) => V.id === id);
|
||||||
|
|
||||||
|
if (!curVersion?.spec?.vm) return;
|
||||||
|
|
||||||
const cloneVersionVM = clone(curVersion.spec.vm);
|
const cloneVersionVM = clone(curVersion.spec.vm);
|
||||||
|
|
||||||
delete cloneVersionVM.spec?.template?.spec?.accessCredentials;
|
delete cloneVersionVM.spec?.template?.spec?.accessCredentials;
|
||||||
@@ -244,7 +247,6 @@ export default {
|
|||||||
delete cloneVersionVM.spec?.template?.metadata?.annotations?.[HCI_ANNOTATIONS.DYNAMIC_SSHKEYS_USERS];
|
delete cloneVersionVM.spec?.template?.metadata?.annotations?.[HCI_ANNOTATIONS.DYNAMIC_SSHKEYS_USERS];
|
||||||
|
|
||||||
const claimTemplate = parseVolumeClaimTemplates(cloneVersionVM);
|
const claimTemplate = parseVolumeClaimTemplates(cloneVersionVM);
|
||||||
|
|
||||||
const deleteDataSource = claimTemplate.map((volume) => {
|
const deleteDataSource = claimTemplate.map((volume) => {
|
||||||
if (volume?.spec?.dataSource) {
|
if (volume?.spec?.dataSource) {
|
||||||
delete volume.spec.dataSource;
|
delete volume.spec.dataSource;
|
||||||
@@ -255,10 +257,16 @@ export default {
|
|||||||
|
|
||||||
cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
||||||
|
|
||||||
// Update labels, instance labels and annotations
|
// Update instance labels, labels and annotations
|
||||||
this.value.metadata.labels = cloneVersionVM.metadata.labels;
|
this.value.spec.template.metadata.labels = cloneVersionVM.spec.template.metadata.labels || {};
|
||||||
this.value.spec.template.metadata.labels = cloneVersionVM.spec.template.metadata.labels;
|
this.value.metadata.labels = {
|
||||||
this.value.metadata.annotations = cloneVersionVM.metadata.annotations;
|
...(curVersion.metadata?.labels || {}),
|
||||||
|
...(cloneVersionVM.metadata?.labels || {})
|
||||||
|
};
|
||||||
|
this.value.metadata.annotations = {
|
||||||
|
...(curVersion.metadata?.annotations || {}),
|
||||||
|
...(cloneVersionVM.metadata?.annotations || {})
|
||||||
|
};
|
||||||
|
|
||||||
this.getInitConfig({
|
this.getInitConfig({
|
||||||
value: cloneVersionVM, existUserData: true, fromTemplate: true
|
value: cloneVersionVM, existUserData: true, fromTemplate: true
|
||||||
@@ -458,7 +466,9 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const errors = this.getAccessCredentialsValidation();
|
const cpuMemoryErrors = this.getCPUMemoryValidation();
|
||||||
|
const accessCredentialsErrors = this.getAccessCredentialsValidation();
|
||||||
|
const errors = [...cpuMemoryErrors, ...accessCredentialsErrors];
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
return Promise.reject(errors);
|
return Promise.reject(errors);
|
||||||
@@ -596,8 +606,11 @@ export default {
|
|||||||
>
|
>
|
||||||
<CpuMemory
|
<CpuMemory
|
||||||
:cpu="cpu"
|
:cpu="cpu"
|
||||||
|
:max-cpu="maxCpu"
|
||||||
:memory="memory"
|
:memory="memory"
|
||||||
|
:max-memory="maxMemory"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
|
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
||||||
@updateCpuMemory="updateCpuMemory"
|
@updateCpuMemory="updateCpuMemory"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -136,13 +136,31 @@ harvester:
|
|||||||
delete: Delete
|
delete: Delete
|
||||||
bundle:
|
bundle:
|
||||||
title: Generate a Support Bundle
|
title: Generate a Support Bundle
|
||||||
|
titleDescription: Collect system-related logs in Harvester to assist with troubleshooting and support.
|
||||||
|
tip: Each field below specifies an aspect of the support bundle. For detailed explanations, please refer to the <a href="{doc}" target="_blank">documentation</a>.
|
||||||
url: Issue URL
|
url: Issue URL
|
||||||
description: Description
|
description: Description
|
||||||
requiredDesc: Description is required!
|
namespaces:
|
||||||
titleDescription: Collect system-related logs in Harvester to help with troubleshooting and support.
|
label: Namespaces
|
||||||
|
tooltip: Select additional namespaces to include in the support bundle.<br/>See docs support-bundle-namespaces for detail.
|
||||||
|
selectAll: 'Select All'
|
||||||
|
unselectAll: 'Unselect All'
|
||||||
|
timeout:
|
||||||
|
label: Timeout
|
||||||
|
tooltip: Minutes allows for completion of the support bundle generation process.<br/>See docs support-bundle-timeout for detail.
|
||||||
|
expiration:
|
||||||
|
label: Expiration
|
||||||
|
tooltip: Minutes before deleting packaged but not downloaded support bundle.<br/>See docs support-bundle-expiration for detail.
|
||||||
|
nodeTimeout:
|
||||||
|
label: Node Collection Timeout
|
||||||
|
tooltip: Minutes allowed for collecting logs/configurations on nodes.<br/>See docs support-bundle-node-collection-timeout for detail.
|
||||||
hotplug:
|
hotplug:
|
||||||
success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
|
success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
|
||||||
title: Add Volume
|
title: Add Volume
|
||||||
|
cpuMemoryHotplug:
|
||||||
|
success: 'CPU and Memory are updated to the virtual machine { vm }.'
|
||||||
|
title: Edit CPU and Memory
|
||||||
|
maxResourcesMessage: 'You can increase the CPU to maximum { maxCpu }C and memory to maximum { maxMemory }.'
|
||||||
hotunplug:
|
hotunplug:
|
||||||
success: 'Volume { name } is detached successfully.'
|
success: 'Volume { name } is detached successfully.'
|
||||||
snapshot:
|
snapshot:
|
||||||
@@ -199,6 +217,7 @@ harvester:
|
|||||||
suspendSchedule: Suspend
|
suspendSchedule: Suspend
|
||||||
restoreExistingVM: Replace Existing
|
restoreExistingVM: Replace Existing
|
||||||
migrate: Migrate
|
migrate: Migrate
|
||||||
|
cpuAndMemoryHotplug: Edit CPU and Memory
|
||||||
abortMigration: Abort Migration
|
abortMigration: Abort Migration
|
||||||
createTemplate: Generate Template
|
createTemplate: Generate Template
|
||||||
enableMaintenance: Enable Maintenance Mode
|
enableMaintenance: Enable Maintenance Mode
|
||||||
@@ -382,6 +401,8 @@ harvester:
|
|||||||
ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
||||||
hash:
|
hash:
|
||||||
sha512: 'Invalid SHA512 checksum.'
|
sha512: 'Invalid SHA512 checksum.'
|
||||||
|
subnet:
|
||||||
|
excludeIps: 'Exclude IPs cannot be empty. Please remove or fill in the exclude IPs.'
|
||||||
|
|
||||||
dashboard:
|
dashboard:
|
||||||
label: Dashboard
|
label: Dashboard
|
||||||
@@ -555,6 +576,10 @@ harvester:
|
|||||||
virtualMachine:
|
virtualMachine:
|
||||||
label: Virtual Machines
|
label: Virtual Machines
|
||||||
osType: OS Type
|
osType: OS Type
|
||||||
|
hotplug:
|
||||||
|
title: Enable CPU and memory hotplug
|
||||||
|
tooltip: The default maximum CPU and maximum memory are {hotPlugTimes} times based on CPU and memory.
|
||||||
|
restartVMMessage: Restart action is required for the virtual machine configuration change to take effect
|
||||||
instance:
|
instance:
|
||||||
singleInstance:
|
singleInstance:
|
||||||
multipleInstance:
|
multipleInstance:
|
||||||
@@ -680,7 +705,7 @@ harvester:
|
|||||||
network:
|
network:
|
||||||
label: Network Data Template
|
label: Network Data Template
|
||||||
title: "Network Data:"
|
title: "Network Data:"
|
||||||
tip: "The network-data configuration allows you to customize the instance’s networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>"
|
tip: "The network-data configuration allows you to customize the instance's networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>"
|
||||||
scheduling:
|
scheduling:
|
||||||
affinity:
|
affinity:
|
||||||
anyNode: 'Run virtual machine on any available node'
|
anyNode: 'Run virtual machine on any available node'
|
||||||
@@ -700,6 +725,9 @@ harvester:
|
|||||||
invalidUser: Invalid Username.
|
invalidUser: Invalid Username.
|
||||||
input:
|
input:
|
||||||
name: Name
|
name: Name
|
||||||
|
cpu: CPU
|
||||||
|
maxCpu: Maximum CPU
|
||||||
|
maxMemory: Maximum Memory
|
||||||
memory: Memory
|
memory: Memory
|
||||||
image: Image
|
image: Image
|
||||||
sshKey: SSHKey
|
sshKey: SSHKey
|
||||||
@@ -864,6 +892,8 @@ harvester:
|
|||||||
uploading: "{name} is uploading, please do not refresh or close the page."
|
uploading: "{name} is uploading, please do not refresh or close the page."
|
||||||
checksum: Checksum
|
checksum: Checksum
|
||||||
checksumTip: Validate the image using the SHA512 checksum, if specified.
|
checksumTip: Validate the image using the SHA512 checksum, if specified.
|
||||||
|
tooltip:
|
||||||
|
imported: Created automatically by the vm-import-controller
|
||||||
|
|
||||||
vmTemplate:
|
vmTemplate:
|
||||||
label: Templates
|
label: Templates
|
||||||
@@ -999,6 +1029,59 @@ harvester:
|
|||||||
progress: Restore in progress
|
progress: Restore in progress
|
||||||
complete: Restore completed
|
complete: Restore completed
|
||||||
|
|
||||||
|
subnet:
|
||||||
|
cidrBlock:
|
||||||
|
tooltip: The subnet range in CIDR notation. Note that the CIDR blocks of different Subnets' within the same VPC cannot overlap.
|
||||||
|
label: CIDR Block
|
||||||
|
placeholder: e.g. 172.20.0.0/16
|
||||||
|
protocol:
|
||||||
|
label: Protocol
|
||||||
|
provider:
|
||||||
|
tooltip: Network provider for this Subnet. Must be one of the Virtual Machine Networks in OverlayNetwork type.
|
||||||
|
label: Provider
|
||||||
|
vpc:
|
||||||
|
label: Virtual Private Cloud
|
||||||
|
gateway:
|
||||||
|
label: Gateway IP
|
||||||
|
placeholder: e.g. 172.20.0.1
|
||||||
|
private:
|
||||||
|
label: Private Subnet
|
||||||
|
tooltip: Enable network isolation for this Subnet. When enabled, VMs can only communicate within this subnet, even if other subnets exist under the same VPC.
|
||||||
|
allowSubnet:
|
||||||
|
label: Allow Subnets
|
||||||
|
tooltip: You can specify certain subnets which can communicate with this subnet.
|
||||||
|
addSubnet: Add Allow Subnet
|
||||||
|
placeholder: e.g. 172.16.0.0/16
|
||||||
|
excludeIPs:
|
||||||
|
tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list.
|
||||||
|
|
||||||
|
vpc:
|
||||||
|
noAddonEnabled:
|
||||||
|
prefix: The kubeovn-operator add-on is not enabled, click
|
||||||
|
middle: here
|
||||||
|
suffix: to enable the add-on to successfully create VPC and subnet.
|
||||||
|
label: Virtual Private Cloud
|
||||||
|
noChild: There is no subnet defined in this Virtual Private Cloud.
|
||||||
|
createSubnet: Create Subnet
|
||||||
|
staticRoutes:
|
||||||
|
label: Static Routes
|
||||||
|
cidr:
|
||||||
|
label: CIDR
|
||||||
|
placeholder: e.g. 172.16.0.0/16
|
||||||
|
nextHopIP:
|
||||||
|
tooltip: The localConnectIP on the other end of the peering VPC.
|
||||||
|
label: Next Hop IP
|
||||||
|
placeholder: e.g. 169.254.0.2
|
||||||
|
vpcPeerings:
|
||||||
|
label: VPC Peerings
|
||||||
|
localConnectIP:
|
||||||
|
tooltip: The designated IP address in CIDR notation of the VPC peering endpoint. Note that both IP addresses (for the VPC and the other VPC) should belong to the same subnet range and should not conflict with the cidrBlock in any existing subnets.
|
||||||
|
label: Local Connect IP
|
||||||
|
placeholder: e.g. 169.254.0.1/16
|
||||||
|
remoteVpc:
|
||||||
|
label: Remote VPC
|
||||||
|
|
||||||
|
|
||||||
network:
|
network:
|
||||||
label: Virtual Machine Networks
|
label: Virtual Machine Networks
|
||||||
tabs:
|
tabs:
|
||||||
@@ -1055,6 +1138,9 @@ harvester:
|
|||||||
publicCertificate: Public Certificate
|
publicCertificate: Public Certificate
|
||||||
privateKey: Private Key
|
privateKey: Private Key
|
||||||
ca: CA
|
ca: CA
|
||||||
|
rancherCluster:
|
||||||
|
kubeConfig: Rancher KubeConfig
|
||||||
|
removeUpstreamClusterWhenNamespaceIsDeleted: Remove Upstream Cluster When Namespace Is Deleted
|
||||||
storageNetwork:
|
storageNetwork:
|
||||||
range:
|
range:
|
||||||
placeholder: e.g. 172.16.0.0/24
|
placeholder: e.g. 172.16.0.0/24
|
||||||
@@ -1063,14 +1149,28 @@ harvester:
|
|||||||
clusterNetwork: Cluster Network
|
clusterNetwork: Cluster Network
|
||||||
vlan: VLAN ID
|
vlan: VLAN ID
|
||||||
exclude:
|
exclude:
|
||||||
label: Exclude
|
label: Exclude IPs
|
||||||
placeholder: e.g. 172.16.0.1/32
|
placeholder: e.g. 172.16.0.1
|
||||||
invalid: '"Exclude list" is invalid.'
|
invalid: '"Exclude list" is invalid.'
|
||||||
addIp: Add Exclude IP
|
addIp: Add Exclude IP
|
||||||
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
||||||
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
|
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
|
||||||
vmForceDeletionPolicy:
|
vmForceDeletionPolicy:
|
||||||
period: Period
|
period: Period
|
||||||
|
vmMigrationNetwork:
|
||||||
|
parseError: "Failed to parse existing configuration."
|
||||||
|
fetchError: "Failed to load required network resources: {error}. Please refresh the page or try again later."
|
||||||
|
clusterNetwork: Cluster Network
|
||||||
|
vlan: VLAN ID
|
||||||
|
range:
|
||||||
|
placeholder: e.g. 172.16.0.0/24
|
||||||
|
label: IP Range
|
||||||
|
invalid: '"Range" is invalid.'
|
||||||
|
exclude:
|
||||||
|
label: Excluded IPs
|
||||||
|
placeholder: e.g. 172.16.0.1/32
|
||||||
|
invalid: '"Exclude list" is invalid.'
|
||||||
|
addButton: Add Exclude IP
|
||||||
ratio : Ratio
|
ratio : Ratio
|
||||||
autoRotateRKE2Certs:
|
autoRotateRKE2Certs:
|
||||||
expiringInHours: Expiring in
|
expiringInHours: Expiring in
|
||||||
@@ -1084,6 +1184,11 @@ harvester:
|
|||||||
provisioner: Provisioner
|
provisioner: Provisioner
|
||||||
volumeSnapshotClassName: Volume Snapshot Class Name
|
volumeSnapshotClassName: Volume Snapshot Class Name
|
||||||
backupVolumeSnapshotClassName: Backup Volume Snapshot Class Name
|
backupVolumeSnapshotClassName: Backup Volume Snapshot Class Name
|
||||||
|
csiOnlineExpandValidation:
|
||||||
|
provisioner: Provisioner
|
||||||
|
value: Value
|
||||||
|
invalidJsonFormat: "Configuration value is not a valid JSON format: {error}"
|
||||||
|
failedToLoadDrivers: "Failed to load CSI drivers. Error: {error}"
|
||||||
containerdRegistry:
|
containerdRegistry:
|
||||||
mirrors:
|
mirrors:
|
||||||
mirrors: Mirrors
|
mirrors: Mirrors
|
||||||
@@ -1114,6 +1219,10 @@ harvester:
|
|||||||
uploadSuccess: "{name} uploaded successfully. Press Upgrade button to start the cluster upgrade process."
|
uploadSuccess: "{name} uploaded successfully. Press Upgrade button to start the cluster upgrade process."
|
||||||
deleteImage: Please select an image to delete.
|
deleteImage: Please select an image to delete.
|
||||||
deleteSuccess: "{name} deleted successfully."
|
deleteSuccess: "{name} deleted successfully."
|
||||||
|
imagePreloadStrategy: Image Preload Strategy
|
||||||
|
restoreVM: Restore VM
|
||||||
|
strategyType: Strategy Type
|
||||||
|
concurrency: Concurrency
|
||||||
harvesterMonitoring:
|
harvesterMonitoring:
|
||||||
label: Harvester Monitoring
|
label: Harvester Monitoring
|
||||||
section:
|
section:
|
||||||
@@ -1275,6 +1384,10 @@ harvester:
|
|||||||
allowedTopologies:
|
allowedTopologies:
|
||||||
title: Allowed Topologies
|
title: Allowed Topologies
|
||||||
tooltip: Allowed Topologies helps scheduling virtual machines on hosts which match all of below expressions.
|
tooltip: Allowed Topologies helps scheduling virtual machines on hosts which match all of below expressions.
|
||||||
|
internal:
|
||||||
|
label: Internal Storage Class
|
||||||
|
cannotDeleteTooltip: Internal storage class volumes cannot be deleted
|
||||||
|
cannotDeleteOrDefaultTooltip: Internal storage classes cannot be deleted or set as default
|
||||||
|
|
||||||
vlanConfig:
|
vlanConfig:
|
||||||
title: Network Configuration
|
title: Network Configuration
|
||||||
@@ -1579,6 +1692,7 @@ advancedSettings:
|
|||||||
'harv-backup-target': Custom backup target to store virtual machine backups.
|
'harv-backup-target': Custom backup target to store virtual machine backups.
|
||||||
'branding': Branding allows administrators to globally re-brand the UI by customizing the Harvester product name, logos, and color scheme.
|
'branding': Branding allows administrators to globally re-brand the UI by customizing the Harvester product name, logos, and color scheme.
|
||||||
'harv-csi-driver-config': Configure additional information for CSI drivers.
|
'harv-csi-driver-config': Configure additional information for CSI drivers.
|
||||||
|
'harv-csi-online-expand-validation': Allow online volume expansion for specific CSI drivers.
|
||||||
'harv-containerd-registry': Containerd Registry Configuration to connect private registries.
|
'harv-containerd-registry': Containerd Registry Configuration to connect private registries.
|
||||||
'harv-log-level': Configure Harvester server log level. Defaults to Info.
|
'harv-log-level': Configure Harvester server log level. Defaults to Info.
|
||||||
'harv-server-version': Harvester server version.
|
'harv-server-version': Harvester server version.
|
||||||
@@ -1597,7 +1711,7 @@ advancedSettings:
|
|||||||
'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down.
|
'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down.
|
||||||
'harv-ssl-parameters': Custom SSL Parameters for TLS validation.
|
'harv-ssl-parameters': Custom SSL Parameters for TLS validation.
|
||||||
'harv-storage-network': 'Longhorn storage-network setting.'
|
'harv-storage-network': 'Longhorn storage-network setting.'
|
||||||
'harv-support-bundle-namespaces': Specify resources in other namespaces to be collected by the support package.
|
'harv-support-bundle-namespaces': Select additional namespaces to include in the support bundle.
|
||||||
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage.
|
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage.
|
||||||
'harv-support-bundle-image': Support bundle image configuration. Find different versions in <a href="https://hub.docker.com/r/rancher/support-bundle-kit/tags" target="_blank">rancher/support-bundle-kit</a>.
|
'harv-support-bundle-image': Support bundle image configuration. Find different versions in <a href="https://hub.docker.com/r/rancher/support-bundle-kit/tags" target="_blank">rancher/support-bundle-kit</a>.
|
||||||
'harv-release-download-url': This setting allows you to configure the <code>upgrade release download</code> URL address. Harvester will get the ISO URL and checksum value from the (<code>$URL</code>/<code>$VERSION</code>/version.yaml) file hosted by the configured URL.
|
'harv-release-download-url': This setting allows you to configure the <code>upgrade release download</code> URL address. Harvester will get the ISO URL and checksum value from the (<code>$URL</code>/<code>$VERSION</code>/version.yaml) file hosted by the configured URL.
|
||||||
@@ -1607,6 +1721,10 @@ advancedSettings:
|
|||||||
'harv-kubeconfig-default-token-ttl-minutes': 'TTL (in minutes) applied on Harvester administration kubeconfig files. Default is 0, which means to never expire.'
|
'harv-kubeconfig-default-token-ttl-minutes': 'TTL (in minutes) applied on Harvester administration kubeconfig files. Default is 0, which means to never expire.'
|
||||||
'harv-longhorn-v2-data-engine-enabled': 'Enable the Longhorn V2 data engine. Default is false. <ul><li>Changing this setting will restart RKE2 on all nodes. This will not affect running VM workloads.</li><li>If you see "not enough hugepages-2Mi capacity" errors when enabling this setting, wait a minute for the error to clear. If the error remains, reboot the affected node.</li></ul>'
|
'harv-longhorn-v2-data-engine-enabled': 'Enable the Longhorn V2 data engine. Default is false. <ul><li>Changing this setting will restart RKE2 on all nodes. This will not affect running VM workloads.</li><li>If you see "not enough hugepages-2Mi capacity" errors when enabling this setting, wait a minute for the error to clear. If the error remains, reboot the affected node.</li></ul>'
|
||||||
'harv-additional-guest-memory-overhead-ratio': 'The ratio for kubevirt to adjust the VM overhead memory. The value could be zero, empty value or floating number between 1.0 and 10.0, default to 1.5.'
|
'harv-additional-guest-memory-overhead-ratio': 'The ratio for kubevirt to adjust the VM overhead memory. The value could be zero, empty value or floating number between 1.0 and 10.0, default to 1.5.'
|
||||||
|
'harv-upgrade-config': 'Configure image preloading and VM restore options for upgrades. See related fields in <a href="{url}" target="_blank" rel="noopener">settings/upgrade-config</a>'
|
||||||
|
'harv-vm-migration-network': 'Segregated network for VM migration traffic.'
|
||||||
|
'harv-rancher-cluster': 'Configure Rancher cluster integration settings for guest cluster management.'
|
||||||
|
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
|
||||||
|
|
||||||
typeLabel:
|
typeLabel:
|
||||||
kubevirt.io.virtualmachine: |-
|
kubevirt.io.virtualmachine: |-
|
||||||
@@ -1664,6 +1782,11 @@ typeLabel:
|
|||||||
one { Virtual Machine Backup }
|
one { Virtual Machine Backup }
|
||||||
other { Virtual Machine Backups }
|
other { Virtual Machine Backups }
|
||||||
}
|
}
|
||||||
|
kubeovn.io.vpc: |-
|
||||||
|
{count, plural,
|
||||||
|
one { Virtual Private Cloud }
|
||||||
|
other { Virtual Private Clouds }
|
||||||
|
}
|
||||||
harvesterhci.io.cloudtemplate: |-
|
harvesterhci.io.cloudtemplate: |-
|
||||||
{count, plural,
|
{count, plural,
|
||||||
one { Cloud Configuration Template }
|
one { Cloud Configuration Template }
|
||||||
|
|||||||
@@ -116,7 +116,15 @@ export default {
|
|||||||
return {
|
return {
|
||||||
...s,
|
...s,
|
||||||
description: isHarvester ? `advancedSettings.descriptions.harv-${ s.id }` : `advancedSettings.descriptions.${ s.id }`,
|
description: isHarvester ? `advancedSettings.descriptions.harv-${ s.id }` : `advancedSettings.descriptions.${ s.id }`,
|
||||||
customized: (!s.readOnly && s.data.value && s.data.value !== s.data.default) || s.data.hasCustomized
|
customized: (!s.readOnly && s.data.value && (
|
||||||
|
s.kind === 'json' ? (() => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(s.data.value)) !== JSON.stringify(JSON.parse(s.data.default));
|
||||||
|
} catch {
|
||||||
|
return s.data.value !== s.data.default;
|
||||||
|
}
|
||||||
|
})() : s.data.value !== s.data.default
|
||||||
|
)) || s.data.hasCustomized
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,34 @@ export default {
|
|||||||
:rows="rows"
|
:rows="rows"
|
||||||
:schema="schema"
|
:schema="schema"
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
/>
|
>
|
||||||
|
<template #cell:name="{ row }">
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
<router-link
|
||||||
|
v-if="row?.detailLocation"
|
||||||
|
:to="row.detailLocation"
|
||||||
|
>
|
||||||
|
{{ row.nameDisplay }}
|
||||||
|
<i
|
||||||
|
v-if="row.isInternalStorageClass && typeof row.isInternalStorageClass === 'function' ? row.isInternalStorageClass() : false"
|
||||||
|
v-clean-tooltip="t('harvester.storage.internal.cannotDeleteOrDefaultTooltip')"
|
||||||
|
class="icon icon-info text-info"
|
||||||
|
style="margin-left: 0.4em;"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
<span v-else>
|
||||||
|
{{ row.nameDisplay }}
|
||||||
|
<i
|
||||||
|
v-if="row.isInternalStorageClass && typeof row.isInternalStorageClass === 'function' ? row.isInternalStorageClass() : false"
|
||||||
|
v-clean-tooltip="t('harvester.storage.internal.cannotDeleteOrDefaultTooltip')"
|
||||||
|
class="icon icon-info text-info"
|
||||||
|
style="margin-left: 0.4em;"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
</ResourceTable>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import ResourceTable from '@shell/components/ResourceTable';
|
|||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { defaultTableSortGenerationFn } from '@shell/components/ResourceTable.vue';
|
import { defaultTableSortGenerationFn } from '@shell/components/ResourceTable.vue';
|
||||||
import FilterLabel from '../components/FilterLabel';
|
import FilterLabel from '../components/FilterLabel';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '../config/labels-annotations';
|
||||||
|
import { isInternalStorageClass } from '../utils/storage-class';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ListHarvesterImage',
|
name: 'ListHarvesterImage',
|
||||||
@@ -53,6 +55,13 @@ export default {
|
|||||||
|
|
||||||
return base;
|
return base;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isInternalStorageClass(row) {
|
||||||
|
const name = row?.spec?.targetStorageClassName ||
|
||||||
|
row?.metadata?.annotations?.[HCI_ANNOTATIONS.STORAGE_CLASS];
|
||||||
|
|
||||||
|
return isInternalStorageClass(name);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -90,6 +99,11 @@ export default {
|
|||||||
v-if="row.isEncrypted"
|
v-if="row.isEncrypted"
|
||||||
class="icon icon-lock"
|
class="icon icon-lock"
|
||||||
/>
|
/>
|
||||||
|
<i
|
||||||
|
v-if="row.isImportedImage"
|
||||||
|
v-clean-tooltip="t('harvester.image.tooltip.imported')"
|
||||||
|
class="icon icon-info"
|
||||||
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ row.nameDisplay }}
|
{{ row.nameDisplay }}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
|
|||||||
import HarvesterVolumeState from '../formatters/HarvesterVolumeState';
|
import HarvesterVolumeState from '../formatters/HarvesterVolumeState';
|
||||||
import { allSettled } from '../utils/promise';
|
import { allSettled } from '../utils/promise';
|
||||||
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
||||||
|
import { INTERNAL_STORAGE_CLASS } from '../config/types';
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
id: HCI.VOLUME,
|
id: HCI.VOLUME,
|
||||||
@@ -130,6 +131,10 @@ export default {
|
|||||||
|
|
||||||
getVMName(row) {
|
getVMName(row) {
|
||||||
return row.attachVM?.metadata?.name || '';
|
return row.attachVM?.metadata?.name || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
isInternalStorageClass(storageClassName) {
|
||||||
|
return this.$store.getters['type-map/labelFor'](INTERNAL_STORAGE_CLASS, storageClassName);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -170,7 +175,7 @@ export default {
|
|||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #col:name="{row}">
|
<template #col:name="{ row }">
|
||||||
<td>
|
<td>
|
||||||
<span>
|
<span>
|
||||||
<router-link
|
<router-link
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
<script>
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import ResourceTable from '@shell/components/ResourceTable';
|
||||||
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
||||||
|
import { NAME, AGE, NAMESPACE, STATE } from '@shell/config/table-headers';
|
||||||
|
import { PROVIDER, PROTOCOL, CIDR_BLOCK } from '@pkg/harvester/config/table-headers';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import { VPC } from '../config/query-params';
|
||||||
|
import { ADD_ONS } from '../config/harvester-map';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
|
import Banner from '@components/Banner/Banner.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterVPC',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ResourceTable,
|
||||||
|
Loading,
|
||||||
|
MessageLink,
|
||||||
|
Banner
|
||||||
|
},
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
schema: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const hash = await allHash({ addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }) });
|
||||||
|
|
||||||
|
this.enabledKubeOvnAddon = hash.addons.find((addon) => addon.name === ADD_ONS.KUBEOVN_OPERATOR)?.spec?.enabled === true;
|
||||||
|
|
||||||
|
if (this.enabledKubeOvnAddon) {
|
||||||
|
try {
|
||||||
|
await allHash({
|
||||||
|
rows: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SUBNET }),
|
||||||
|
vpcs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
|
||||||
|
});
|
||||||
|
this.$store.dispatch('type-map/configureType', { match: HCI.SUBNET, isCreatable: this.enabledKubeOvnAddon });
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Error fetching VPC and Subnet data:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
HCI,
|
||||||
|
hasBothSchema: false,
|
||||||
|
enabledKubeOvnAddon: false,
|
||||||
|
to: `${ HCI.ADD_ONS }/kube-system/${ ADD_ONS.KUBEOVN_OPERATOR }?mode=edit`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
headers() {
|
||||||
|
return [
|
||||||
|
STATE,
|
||||||
|
NAME,
|
||||||
|
NAMESPACE,
|
||||||
|
CIDR_BLOCK,
|
||||||
|
PROTOCOL,
|
||||||
|
PROVIDER,
|
||||||
|
AGE
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
rows() {
|
||||||
|
return this.$store.getters[`harvester/all`](HCI.SUBNET) || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
vpcWithoutSubnets() {
|
||||||
|
const vpcs = this.$store.getters[`harvester/all`](HCI.VPC) || [];
|
||||||
|
|
||||||
|
const out = vpcs.map((v) => {
|
||||||
|
const hasChild = v.status?.subnets?.length > 0 || false;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...v,
|
||||||
|
hasChild
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
isSubnetCreatable() {
|
||||||
|
return (this.subnetSchema?.collectionMethods || []).includes('POST');
|
||||||
|
},
|
||||||
|
|
||||||
|
rowsWithFakeVpcs() {
|
||||||
|
const fakeRows = this.vpcWithoutSubnets.map((vpc) => {
|
||||||
|
return {
|
||||||
|
groupByLabel: vpc.id,
|
||||||
|
isFake: true,
|
||||||
|
mainRowKey: vpc.id,
|
||||||
|
nameDisplay: vpc.id,
|
||||||
|
groupByVpc: vpc.id,
|
||||||
|
availableActions: []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...this.rows, ...fakeRows];
|
||||||
|
},
|
||||||
|
|
||||||
|
createVPCLocation() {
|
||||||
|
const location = {
|
||||||
|
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
|
||||||
|
params: {
|
||||||
|
product: HARVESTER_PRODUCT,
|
||||||
|
resource: HCI.VPC,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return location;
|
||||||
|
},
|
||||||
|
|
||||||
|
vpcSchema() {
|
||||||
|
return this.$store.getters[`harvester/schemaFor`](HCI.VPC);
|
||||||
|
},
|
||||||
|
|
||||||
|
subnetSchema() {
|
||||||
|
return this.$store.getters[`harvester/schemaFor`](HCI.SUBNET);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
groupLabel(group) {
|
||||||
|
return `${ this.t('harvester.vpc.label') }: ${ group.key }`;
|
||||||
|
},
|
||||||
|
|
||||||
|
slotName(vpc) {
|
||||||
|
return `main-row:${ vpc }`;
|
||||||
|
},
|
||||||
|
|
||||||
|
createSubnetLocation(group) {
|
||||||
|
const vpc = group.key;
|
||||||
|
|
||||||
|
const location = {
|
||||||
|
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
|
||||||
|
params: {
|
||||||
|
product: HARVESTER_PRODUCT,
|
||||||
|
resource: HCI.SUBNET,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
location.query = { [VPC]: vpc };
|
||||||
|
|
||||||
|
return location;
|
||||||
|
},
|
||||||
|
|
||||||
|
showVpcAction(event, group) {
|
||||||
|
const vpc = group.key;
|
||||||
|
|
||||||
|
const resource = this.$store.getters[`harvester/byId`](HCI.VPC, vpc);
|
||||||
|
|
||||||
|
this.$store.commit(`action-menu/show`, {
|
||||||
|
resources: [resource],
|
||||||
|
elem: event.target
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
typeDisplay() {
|
||||||
|
return this.t('harvester.vpc.label');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<div v-else-if="!enabledKubeOvnAddon">
|
||||||
|
<Banner color="warning">
|
||||||
|
<MessageLink
|
||||||
|
:to="to"
|
||||||
|
prefix-label="harvester.vpc.noAddonEnabled.prefix"
|
||||||
|
middle-label="harvester.vpc.noAddonEnabled.middle"
|
||||||
|
suffix-label="harvester.vpc.noAddonEnabled.suffix"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<Masthead
|
||||||
|
:schema="vpcSchema"
|
||||||
|
:type-display="t('harvester.vpc.label')"
|
||||||
|
:resource="HCI.VPC"
|
||||||
|
:create-location="createVPCLocation"
|
||||||
|
:create-button-label="t('harvester.clusterNetwork.create.button.label')"
|
||||||
|
/>
|
||||||
|
<ResourceTable
|
||||||
|
:rows="rowsWithFakeVpcs"
|
||||||
|
:headers="headers"
|
||||||
|
:schema="subnetSchema"
|
||||||
|
:groupable="true"
|
||||||
|
group-by="groupByVpc"
|
||||||
|
>
|
||||||
|
<template #header-middle>
|
||||||
|
<div />
|
||||||
|
</template>
|
||||||
|
<template #group-by="{group}">
|
||||||
|
<div class="group-bar">
|
||||||
|
<div class="group-tab">
|
||||||
|
<span>
|
||||||
|
{{ groupLabel(group) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<router-link
|
||||||
|
v-if="isSubnetCreatable"
|
||||||
|
class="btn btn-sm role-secondary mr-5"
|
||||||
|
:to="createSubnetLocation(group)"
|
||||||
|
>
|
||||||
|
{{ t('harvester.vpc.createSubnet') }}
|
||||||
|
</router-link>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm role-multi-action actions mr-10"
|
||||||
|
@click="showVpcAction($event, group)"
|
||||||
|
>
|
||||||
|
<i class="icon icon-actions" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
v-for="(vpc) in vpcWithoutSubnets"
|
||||||
|
:key="vpc.id"
|
||||||
|
v-slot:[slotName(vpc.id)]
|
||||||
|
>
|
||||||
|
<tr
|
||||||
|
v-show="!vpc.hasChild"
|
||||||
|
class="main-row"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="empty text-center"
|
||||||
|
colspan="12"
|
||||||
|
>
|
||||||
|
{{ t('harvester.vpc.noChild') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</ResourceTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.state {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.icon-warning {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.group-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.right {
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-tab {
|
||||||
|
&, &::after {
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
right: -20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
SPAN {
|
||||||
|
color: var(--body-text) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -22,8 +22,8 @@ export const VM_HEADERS = [
|
|||||||
{
|
{
|
||||||
name: 'CPU',
|
name: 'CPU',
|
||||||
label: 'CPU',
|
label: 'CPU',
|
||||||
sort: ['spec.template.spec.domain.cpu.cores'],
|
sort: ['displayCPU'],
|
||||||
value: 'spec.template.spec.domain.cpu.cores',
|
value: 'displayCPU',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
dashIfEmpty: true,
|
dashIfEmpty: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,12 +2,10 @@
|
|||||||
import Loading from '@shell/components/Loading';
|
import Loading from '@shell/components/Loading';
|
||||||
import ResourceTable from '@shell/components/ResourceTable';
|
import ResourceTable from '@shell/components/ResourceTable';
|
||||||
import Masthead from '@shell/components/ResourceList/Masthead';
|
import Masthead from '@shell/components/ResourceList/Masthead';
|
||||||
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { STATE, AGE, NAME } from '@shell/config/table-headers';
|
import { STATE, AGE, NAME } from '@shell/config/table-headers';
|
||||||
import { mapPref, GROUP_RESOURCES } from '@shell/store/prefs';
|
import { mapPref, GROUP_RESOURCES } from '@shell/store/prefs';
|
||||||
import { NODE } from '@shell/config/types';
|
import { NODE } from '@shell/config/types';
|
||||||
|
|
||||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
||||||
import { CLUSTER_NETWORK } from '../config/query-params';
|
import { CLUSTER_NETWORK } from '../config/query-params';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
@@ -38,7 +36,6 @@ export default {
|
|||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
groupPreference: mapPref(GROUP_RESOURCES),
|
groupPreference: mapPref(GROUP_RESOURCES),
|
||||||
|
|
||||||
headers() {
|
headers() {
|
||||||
return [
|
return [
|
||||||
STATE,
|
STATE,
|
||||||
@@ -209,7 +206,6 @@ export default {
|
|||||||
{{ groupLabel(group) }}
|
{{ groupLabel(group) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<router-link
|
<router-link
|
||||||
v-if="isClusterNetworkCreatable && group.key !== 'mgmt'"
|
v-if="isClusterNetworkCreatable && group.key !== 'mgmt'"
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import jsyaml from 'js-yaml';
|
|||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
import difference from 'lodash/difference';
|
import difference from 'lodash/difference';
|
||||||
|
|
||||||
import { sortBy } from '@shell/utils/sort';
|
import { sortBy } from '@shell/utils/sort';
|
||||||
import { set } from '@shell/utils/object';
|
import { set } from '@shell/utils/object';
|
||||||
|
import { getVmCPUMemoryValues } from '../../utils/cpuMemory';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { randomStr } from '@shell/utils/string';
|
import { randomStr } from '@shell/utils/string';
|
||||||
import { base64Decode } from '@shell/utils/crypto';
|
import { base64Decode } from '@shell/utils/crypto';
|
||||||
@@ -164,6 +163,9 @@ export default {
|
|||||||
deleteAgent: true,
|
deleteAgent: true,
|
||||||
memory: null,
|
memory: null,
|
||||||
cpu: '',
|
cpu: '',
|
||||||
|
maxMemory: null,
|
||||||
|
maxCpu: '',
|
||||||
|
cpuMemoryHotplugEnabled: false,
|
||||||
reservedMemory: null,
|
reservedMemory: null,
|
||||||
accessCredentials: [],
|
accessCredentials: [],
|
||||||
efiEnabled: false,
|
efiEnabled: false,
|
||||||
@@ -348,8 +350,11 @@ export default {
|
|||||||
const runStrategy = spec.runStrategy || 'RerunOnFailure';
|
const runStrategy = spec.runStrategy || 'RerunOnFailure';
|
||||||
const machineType = spec.template.spec.domain?.machine?.type || this.machineTypes[0];
|
const machineType = spec.template.spec.domain?.machine?.type || this.machineTypes[0];
|
||||||
|
|
||||||
const cpu = spec.template.spec.domain?.cpu?.cores;
|
const {
|
||||||
const memory = spec.template.spec.domain.resources.limits.memory;
|
cpu, memory, maxCpu, maxMemory, isHotplugEnabled
|
||||||
|
} = getVmCPUMemoryValues(vm);
|
||||||
|
const cpuMemoryHotplugEnabled = isHotplugEnabled;
|
||||||
|
|
||||||
const reservedMemory = vm.metadata?.annotations?.[HCI_ANNOTATIONS.VM_RESERVED_MEMORY];
|
const reservedMemory = vm.metadata?.annotations?.[HCI_ANNOTATIONS.VM_RESERVED_MEMORY];
|
||||||
const terminationGracePeriodSeconds = spec.template.spec?.terminationGracePeriodSeconds || this.defaultTerminationSetting;
|
const terminationGracePeriodSeconds = spec.template.spec?.terminationGracePeriodSeconds || this.defaultTerminationSetting;
|
||||||
|
|
||||||
@@ -409,6 +414,9 @@ export default {
|
|||||||
|
|
||||||
this['cpu'] = cpu;
|
this['cpu'] = cpu;
|
||||||
this['memory'] = memory;
|
this['memory'] = memory;
|
||||||
|
this['maxCpu'] = maxCpu;
|
||||||
|
this['maxMemory'] = maxMemory;
|
||||||
|
this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
|
||||||
this['reservedMemory'] = reservedMemory;
|
this['reservedMemory'] = reservedMemory;
|
||||||
this['machineType'] = machineType;
|
this['machineType'] = machineType;
|
||||||
this['terminationGracePeriodSeconds'] = terminationGracePeriodSeconds;
|
this['terminationGracePeriodSeconds'] = terminationGracePeriodSeconds;
|
||||||
@@ -633,20 +641,26 @@ export default {
|
|||||||
this.spec.template.spec.domain.machine['type'] = this.machineType;
|
this.spec.template.spec.domain.machine['type'] = this.machineType;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.spec.template.spec.domain.cpu.cores = this.cpu;
|
this.setCPUAndMemory();
|
||||||
this.spec.template.spec.domain.resources.limits.cpu = this.cpu ? this.cpu.toString() : this.cpu;
|
// update terminationGracePeriodSeconds
|
||||||
this.spec.template.spec.domain.resources.limits.memory = this.memory;
|
|
||||||
this.spec.template.spec.terminationGracePeriodSeconds = this.terminationGracePeriodSeconds;
|
this.spec.template.spec.terminationGracePeriodSeconds = this.terminationGracePeriodSeconds;
|
||||||
|
|
||||||
// parse reserved memory
|
|
||||||
const vm = this.resourceType === HCI.VM ? this.value : this.value.spec.vm;
|
const vm = this.resourceType === HCI.VM ? this.value : this.value.spec.vm;
|
||||||
|
|
||||||
|
// parse reserved memory
|
||||||
if (!this.reservedMemory) {
|
if (!this.reservedMemory) {
|
||||||
delete vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY];
|
delete vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY];
|
||||||
} else {
|
} else {
|
||||||
vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY] = this.reservedMemory;
|
vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY] = this.reservedMemory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add or remove cpu memory hotplug annotation
|
||||||
|
if (this.cpuMemoryHotplugEnabled) {
|
||||||
|
vm.metadata.annotations[HCI_ANNOTATIONS.VM_CPU_MEMORY_HOTPLUG] = this.cpuMemoryHotplugEnabled.toString();
|
||||||
|
} else {
|
||||||
|
delete vm.metadata.annotations[HCI_ANNOTATIONS.VM_CPU_MEMORY_HOTPLUG];
|
||||||
|
}
|
||||||
|
|
||||||
if (this.maintenanceStrategy === 'Migrate') {
|
if (this.maintenanceStrategy === 'Migrate') {
|
||||||
delete vm.metadata.labels[HCI_ANNOTATIONS.VM_MAINTENANCE_MODE_STRATEGY];
|
delete vm.metadata.labels[HCI_ANNOTATIONS.VM_MAINTENANCE_MODE_STRATEGY];
|
||||||
} else {
|
} else {
|
||||||
@@ -654,6 +668,36 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setCPUAndMemory() {
|
||||||
|
if (this.cpuMemoryHotplugEnabled) {
|
||||||
|
// set CPU
|
||||||
|
this.spec.template.spec.domain.cpu.sockets = this.cpu;
|
||||||
|
this.spec.template.spec.domain.cpu.cores = 1;
|
||||||
|
|
||||||
|
// set max CPU
|
||||||
|
set(this.spec.template.spec, 'domain.cpu.maxSockets', this.maxCpu);
|
||||||
|
// domain.resources.limits.cpu and memory are defined by k8s which requires string values
|
||||||
|
// see https://kubernetes.io/docs/tasks/configure-pod-container/assign-cpu-resource/
|
||||||
|
set(this.spec.template.spec, 'domain.resources.limits.cpu', this.maxCpu?.toString());
|
||||||
|
|
||||||
|
// set memory
|
||||||
|
set(this.spec.template.spec, 'domain.memory.guest', this.memory);
|
||||||
|
|
||||||
|
// set max memory
|
||||||
|
set(this.spec.template.spec, 'domain.memory.maxGuest', this.maxMemory);
|
||||||
|
set(this.spec.template.spec, 'domain.resources.limits.memory', this.maxMemory);
|
||||||
|
} else {
|
||||||
|
this.spec.template.spec.domain.cpu.sockets = 1;
|
||||||
|
this.spec.template.spec.domain.cpu.cores = this.cpu;
|
||||||
|
this.spec.template.spec.domain.resources.limits.cpu = this.cpu?.toString();
|
||||||
|
this.spec.template.spec.domain.resources.limits.memory = this.memory;
|
||||||
|
// clean
|
||||||
|
delete this.spec.template.spec.resources;
|
||||||
|
delete this.spec.template.spec.domain.memory;
|
||||||
|
delete this.spec.template.spec.domain.cpu.maxSockets;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
parseDiskRows(disk) {
|
parseDiskRows(disk) {
|
||||||
const disks = [];
|
const disks = [];
|
||||||
const volumes = [];
|
const volumes = [];
|
||||||
@@ -952,9 +996,12 @@ export default {
|
|||||||
this['sshKey'] = neu;
|
this['sshKey'] = neu;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateCpuMemory(cpu, memory) {
|
updateCpuMemory(cpu, memory, maxCpu = '', maxMemory = null, cpuMemoryHotplugEnabled = false) {
|
||||||
this['cpu'] = cpu;
|
this['cpu'] = cpu;
|
||||||
this['memory'] = memory;
|
this['memory'] = memory;
|
||||||
|
this['maxCpu'] = maxCpu;
|
||||||
|
this['maxMemory'] = maxMemory;
|
||||||
|
this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
|
||||||
},
|
},
|
||||||
|
|
||||||
parseDisk(R, index) {
|
parseDisk(R, index) {
|
||||||
@@ -1311,6 +1358,21 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getCPUMemoryValidation() {
|
||||||
|
const errors = [];
|
||||||
|
const { cpu, memory } = this;
|
||||||
|
|
||||||
|
if ((!cpu)) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.cpu') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((!memory)) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.memory') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
},
|
||||||
|
|
||||||
getAccessCredentialsValidation() {
|
getAccessCredentialsValidation() {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { HCI } from '../../types';
|
|||||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
|
||||||
import { LONGHORN_DRIVER } from '@shell/config/types';
|
import { LONGHORN_DRIVER } from '@shell/config/types';
|
||||||
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
|
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
|
||||||
|
import { isInternalStorageClass } from '../../utils/storage-class';
|
||||||
|
|
||||||
export const LVM_DRIVER = 'lvm.driver.harvesterhci.io';
|
export const LVM_DRIVER = 'lvm.driver.harvesterhci.io';
|
||||||
|
|
||||||
@@ -85,4 +86,22 @@ export default class HciStorageClass extends StorageClass {
|
|||||||
get thirdPartyStorageFeatureEnabled() {
|
get thirdPartyStorageFeatureEnabled() {
|
||||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isInternalStorageClass() {
|
||||||
|
return isInternalStorageClass(this.metadata?.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
get availableActions() {
|
||||||
|
let out = super.availableActions || [];
|
||||||
|
|
||||||
|
out = out.map((action) => {
|
||||||
|
if ((action.action === 'setDefault' || action.action === 'setAsDefault' || action.action === 'promptRemove') && this.isInternalStorageClass()) {
|
||||||
|
return { ...action, enabled: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return action;
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,40 +57,42 @@ export default class HciVmImage extends HarvesterResource {
|
|||||||
|
|
||||||
const customActions = this.isReady ? [
|
const customActions = this.isReady ? [
|
||||||
{
|
{
|
||||||
action: 'createFromImage',
|
action: 'createFromImage',
|
||||||
enabled: canCreateVM,
|
enabled: canCreateVM,
|
||||||
icon: 'icon icon-circle-plus',
|
icon: 'icon icon-circle-plus',
|
||||||
label: this.t('harvester.action.createVM'),
|
label: this.t('harvester.action.createVM'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'encryptImage',
|
action: 'encryptImage',
|
||||||
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted,
|
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted,
|
||||||
icon: 'icon icon-lock',
|
icon: 'icon icon-lock',
|
||||||
label: this.t('harvester.action.encryptImage'),
|
label: this.t('harvester.action.encryptImage'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'decryptImage',
|
action: 'decryptImage',
|
||||||
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted,
|
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted,
|
||||||
icon: 'icon icon-unlock',
|
icon: 'icon icon-unlock',
|
||||||
label: this.t('harvester.action.decryptImage'),
|
label: this.t('harvester.action.decryptImage'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'imageDownload',
|
action: 'imageDownload',
|
||||||
enabled: this.links?.download,
|
enabled: this.links?.download,
|
||||||
icon: 'icon icon-download',
|
icon: 'icon icon-download',
|
||||||
label: this.t('asyncButton.download.action'),
|
label: this.t('asyncButton.download.action'),
|
||||||
}
|
}
|
||||||
] : [];
|
] : [];
|
||||||
|
|
||||||
let filteredOut;
|
// imported image only allow download, download yaml and delete actions
|
||||||
|
if (this.isImportedImage) {
|
||||||
|
const custom = customActions.find((a) => a.action === 'imageDownload');
|
||||||
|
const filtered = out.filter(({ action }) => ['download', 'promptRemove'].includes(action));
|
||||||
|
|
||||||
if (customActions.length > 0) {
|
return custom ? [custom, { divider: true }, ...filtered] : filtered;
|
||||||
filteredOut = out;
|
|
||||||
} else {
|
|
||||||
// if the first item is a divider, remove it from the array
|
|
||||||
filteredOut = out[0]?.divider ? out.slice(1) : out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if the first item is a divider, remove it from the array
|
||||||
|
const filteredOut = customActions.length > 0 ? out : (out[0]?.divider ? out.slice(1) : out);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...customActions,
|
...customActions,
|
||||||
...filteredOut
|
...filteredOut
|
||||||
@@ -211,6 +213,10 @@ export default class HciVmImage extends HarvesterResource {
|
|||||||
!!this.spec.securityParameters?.sourceImageNamespace;
|
!!this.spec.securityParameters?.sourceImageNamespace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isImportedImage() {
|
||||||
|
return (this.metadata?.labels?.[HCI_ANNOTATIONS.IMPORTED_IMAGE]) === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
get displayNameWithNamespace() {
|
get displayNameWithNamespace() {
|
||||||
return `${ this.metadata.namespace }/${ this.spec.displayName }`;
|
return `${ this.metadata.namespace }/${ this.spec.displayName }`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { clone } from '@shell/utils/object';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import HarvesterResource from './harvester';
|
||||||
|
|
||||||
|
export default class HciSubnet extends HarvesterResource {
|
||||||
|
get groupByVpc() {
|
||||||
|
return this.spec?.vpc || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
get doneOverride() {
|
||||||
|
const detailLocation = clone(this.listLocation);
|
||||||
|
|
||||||
|
detailLocation.params.resource = HCI.VPC;
|
||||||
|
|
||||||
|
return detailLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
get parentLocationOverride() {
|
||||||
|
return {
|
||||||
|
...this.listLocation,
|
||||||
|
params: {
|
||||||
|
...this.listLocation.params,
|
||||||
|
resource: HCI.VPC
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { clone } from '@shell/utils/object';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import HarvesterResource from './harvester';
|
||||||
|
|
||||||
|
export default class HciVPC extends HarvesterResource {
|
||||||
|
get parentNameOverride() {
|
||||||
|
return this.$rootGetters['i18n/t'](`typeLabel."${ HCI.VPC }"`, { count: 1 })?.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
get doneOverride() {
|
||||||
|
const detailLocation = clone(this.listLocation);
|
||||||
|
|
||||||
|
detailLocation.params.resource = HCI.VPC;
|
||||||
|
|
||||||
|
return detailLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
get parentLocationOverride() {
|
||||||
|
return {
|
||||||
|
...this.listLocation,
|
||||||
|
params: {
|
||||||
|
...this.listLocation.params,
|
||||||
|
resource: HCI.VPC
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import { parseVolumeClaimTemplates } from '@pkg/utils/vm';
|
|||||||
import { BACKUP_TYPE } from '../config/types';
|
import { BACKUP_TYPE } from '../config/types';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import HarvesterResource from './harvester';
|
import HarvesterResource from './harvester';
|
||||||
|
import { getVmCPUMemoryValues } from '../utils/cpuMemory';
|
||||||
|
|
||||||
export const OFF = 'Off';
|
export const OFF = 'Off';
|
||||||
|
|
||||||
@@ -169,6 +170,12 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
icon: 'icon icon-storage',
|
icon: 'icon icon-storage',
|
||||||
label: this.t('harvester.action.editVMQuota')
|
label: this.t('harvester.action.editVMQuota')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
action: 'cpuMemoryHotplug',
|
||||||
|
enabled: !!this.actions?.cpuAndMemoryHotplug,
|
||||||
|
icon: 'icon icon-os-management',
|
||||||
|
label: this.t('harvester.action.cpuAndMemoryHotplug')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
action: 'createSchedule',
|
action: 'createSchedule',
|
||||||
enabled: this.schedulingVMBackupFeatureEnabled,
|
enabled: this.schedulingVMBackupFeatureEnabled,
|
||||||
@@ -476,6 +483,13 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cpuMemoryHotplug(resources = this) {
|
||||||
|
this.$dispatch('promptModal', {
|
||||||
|
resources,
|
||||||
|
component: 'HarvesterCPUMemoryHotPlugDialog'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
abortMigrationVM() {
|
abortMigrationVM() {
|
||||||
this.doActionGrowl('abortMigration', {});
|
this.doActionGrowl('abortMigration', {});
|
||||||
}
|
}
|
||||||
@@ -1091,19 +1105,6 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
maxLength: 63,
|
maxLength: 63,
|
||||||
translationKey: 'harvester.fields.name'
|
translationKey: 'harvester.fields.name'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
nullable: false,
|
|
||||||
path: 'spec.template.spec.domain.cpu.cores',
|
|
||||||
min: 1,
|
|
||||||
required: true,
|
|
||||||
translationKey: 'harvester.fields.cpu'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nullable: false,
|
|
||||||
path: 'spec.template.spec.domain.resources.limits.memory',
|
|
||||||
required: true,
|
|
||||||
translationKey: 'harvester.fields.memory'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
nullable: false,
|
nullable: false,
|
||||||
path: 'spec.template.spec',
|
path: 'spec.template.spec',
|
||||||
@@ -1127,12 +1128,11 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get memorySort() {
|
get memorySort() {
|
||||||
const memory =
|
const memory = getVmCPUMemoryValues(this).memory;
|
||||||
this?.spec?.template?.spec?.domain?.resources?.requests?.memory || 0;
|
|
||||||
|
|
||||||
const formatSize = parseSi(memory);
|
const formatSize = parseSi(memory);
|
||||||
|
|
||||||
return parseInt(formatSize);
|
return parseInt(formatSize, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
get ingoreVMMessage() {
|
get ingoreVMMessage() {
|
||||||
@@ -1158,14 +1158,22 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get stateDescription() {
|
get stateDescription() {
|
||||||
|
const conditions = get(this, 'status.conditions');
|
||||||
|
const restartRequired = findBy(conditions, 'type', 'RestartRequired');
|
||||||
|
|
||||||
|
if (restartRequired && restartRequired.status === 'True') {
|
||||||
|
return this.t('harvester.virtualMachine.hotplug.restartVMMessage');
|
||||||
|
}
|
||||||
|
|
||||||
return this.ingoreVMMessage ? '' : super.stateDescription;
|
return this.ingoreVMMessage ? '' : super.stateDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get displayCPU() {
|
||||||
|
return getVmCPUMemoryValues(this).cpu;
|
||||||
|
}
|
||||||
|
|
||||||
get displayMemory() {
|
get displayMemory() {
|
||||||
return (
|
return getVmCPUMemoryValues(this).memory;
|
||||||
this.spec.template.spec.domain.resources?.limits?.memory ||
|
|
||||||
this.spec.template.spec.domain.resources?.requests?.memory
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get isQemuInstalled() {
|
get isQemuInstalled() {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester",
|
"name": "harvester",
|
||||||
"description": "Rancher UI Extension for Harvester",
|
"description": "Rancher UI Extension for Harvester",
|
||||||
"version": "1.6.0-dev",
|
"version": "1.6.0-rc3",
|
||||||
"private": false,
|
"private": false,
|
||||||
"rancher": {
|
"rancher": {
|
||||||
"annotations": {
|
"annotations": {
|
||||||
"catalog.cattle.io/display-name": "Harvester",
|
"catalog.cattle.io/display-name": "Harvester",
|
||||||
"catalog.cattle.io/kube-version": ">= 1.16.0-0",
|
"catalog.cattle.io/kube-version": ">= 1.16.0-0",
|
||||||
"catalog.cattle.io/rancher-version": ">= 2.11.0-0",
|
"catalog.cattle.io/rancher-version": ">= 2.12.0-0",
|
||||||
"catalog.cattle.io/ui-extensions-version": ">= 3.0.0 < 4.0.0"
|
"catalog.cattle.io/ui-extensions-version": ">= 3.0.0 < 4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<script>
|
||||||
|
import { resourceNames } from '@shell/utils/string';
|
||||||
|
import { mapGetters, mapState } from 'vuex';
|
||||||
|
import { BACKUP_TYPE } from '@pkg/harvester/config/types';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PromptRemoveVMBackupDialog',
|
||||||
|
|
||||||
|
emits: ['errors'],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
default: () => {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
names: {
|
||||||
|
type: Array,
|
||||||
|
default: () => {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
|
||||||
|
close: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
|
||||||
|
doneLocation: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { errors: [] };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState('action-menu', ['toRemove']),
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
resourceType() {
|
||||||
|
const isSnapshot = this.value?.[0]?.spec?.type === BACKUP_TYPE.SNAPSHOT;
|
||||||
|
const count = this.names?.length || 1;
|
||||||
|
|
||||||
|
if (isSnapshot) {
|
||||||
|
return this.t(`typeLabel."${ HCI.VM_SNAPSHOT }"`, { count });
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.t(`typeLabel."${ HCI.BACKUP }"`, { count });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resourceNames,
|
||||||
|
async remove(buttonDone) {
|
||||||
|
try {
|
||||||
|
await Promise.all(this.value.map((resource) => resource.remove()));
|
||||||
|
this.close(buttonDone);
|
||||||
|
} catch (err) {
|
||||||
|
this.errors = exceptionToErrorsArray(err);
|
||||||
|
buttonDone(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mt-10">
|
||||||
|
<div class="mb-5">
|
||||||
|
{{ t('promptRemove.attemptingToRemove', { type: resourceType }) }} <span
|
||||||
|
v-clean-html="resourceNames(names, null, t)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Banner
|
||||||
|
v-for="(error, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
class=""
|
||||||
|
color="error"
|
||||||
|
:label="error"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -16,6 +16,8 @@ export const HCI = {
|
|||||||
RESTORE: 'harvesterhci.io.virtualmachinerestore',
|
RESTORE: 'harvesterhci.io.virtualmachinerestore',
|
||||||
NODE_NETWORK: 'network.harvesterhci.io.nodenetwork',
|
NODE_NETWORK: 'network.harvesterhci.io.nodenetwork',
|
||||||
CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork',
|
CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork',
|
||||||
|
SUBNET: 'kubeovn.io.subnet',
|
||||||
|
VPC: 'kubeovn.io.vpc',
|
||||||
VM_IMAGE_DOWNLOADER: 'harvesterhci.io.virtualmachineimagedownloader',
|
VM_IMAGE_DOWNLOADER: 'harvesterhci.io.virtualmachineimagedownloader',
|
||||||
SUPPORT_BUNDLE: 'harvesterhci.io.supportbundle',
|
SUPPORT_BUNDLE: 'harvesterhci.io.supportbundle',
|
||||||
NETWORK_ATTACHMENT: 'harvesterhci.io.networkattachmentdefinition',
|
NETWORK_ATTACHMENT: 'harvesterhci.io.networkattachmentdefinition',
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
|
||||||
|
export function getVmCPUMemoryValues(vm) {
|
||||||
|
if (!vm) {
|
||||||
|
return {
|
||||||
|
cpu: 0,
|
||||||
|
memory: null,
|
||||||
|
isHotplugEnabled: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHotplugEnabled = isCPUMemoryHotPlugEnabled(vm);
|
||||||
|
|
||||||
|
if (isHotplugEnabled) {
|
||||||
|
return {
|
||||||
|
cpu: vm.spec.template.spec.domain.cpu.sockets,
|
||||||
|
memory: vm.spec.template.spec.domain?.memory?.guest || null,
|
||||||
|
maxCpu: vm.spec.template.spec.domain.cpu?.maxSockets || 0,
|
||||||
|
maxMemory: vm.spec.template.spec.domain?.memory?.maxGuest || null,
|
||||||
|
isHotplugEnabled
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
cpu: vm.spec.template.spec.domain.cpu.cores,
|
||||||
|
memory: vm.spec.template.spec.domain.resources?.limits?.memory || null,
|
||||||
|
isHotplugEnabled
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCPUMemoryHotPlugEnabled(vm) {
|
||||||
|
return vm?.metadata?.annotations[HCI_ANNOTATIONS.VM_CPU_MEMORY_HOTPLUG] === 'true' ||
|
||||||
|
!!vm?.spec?.template?.spec?.domain.cpu?.maxSockets ||
|
||||||
|
!!vm?.spec?.template?.spec?.domain?.memory?.maxGuest ||
|
||||||
|
false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { INTERNAL_STORAGE_CLASS } from '../config/types';
|
||||||
|
|
||||||
|
export function isInternalStorageClass(name) {
|
||||||
|
return name === INTERNAL_STORAGE_CLASS.VMSTATE_PERSISTENCE ||
|
||||||
|
name === INTERNAL_STORAGE_CLASS.LONGHORN_STATIC;
|
||||||
|
}
|
||||||
Executable
+23
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
TAG_VERSION=$1
|
||||||
|
|
||||||
|
if [[ -z "$TAG_VERSION" ]]; then
|
||||||
|
echo "No tag version provided"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION=$(jq -r .version package.json)
|
||||||
|
EXTENSION_VERSION=$(jq -r .version pkg/harvester/package.json)
|
||||||
|
PKG_VERSION="v${EXTENSION_VERSION}"
|
||||||
|
|
||||||
|
if [[ "$VERSION" != "$EXTENSION_VERSION" ]]; then
|
||||||
|
echo "Package version mismatch: $VERSION vs $EXTENSION_VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${TAG_VERSION}" != "$PKG_VERSION" ]]; then
|
||||||
|
echo "Package version $PKG_VERSION in pkg/harvester/package.json does not match tag version ${TAG_VERSION}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Package version and tag version match: ${TAG_VERSION}"
|
||||||
@@ -2986,6 +2986,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@jsep-plugin/regex/-/regex-1.0.4.tgz#cb2fc423220fa71c609323b9ba7f7d344a755fcc"
|
resolved "https://registry.yarnpkg.com/@jsep-plugin/regex/-/regex-1.0.4.tgz#cb2fc423220fa71c609323b9ba7f7d344a755fcc"
|
||||||
integrity sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==
|
integrity sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==
|
||||||
|
|
||||||
|
"@kurkle/color@^0.3.0":
|
||||||
|
version "0.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.4.tgz#4d4ff677e1609214fc71c580125ddddd86abcabf"
|
||||||
|
integrity sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==
|
||||||
|
|
||||||
"@leichtgewicht/ip-codec@^2.0.1":
|
"@leichtgewicht/ip-codec@^2.0.1":
|
||||||
version "2.0.5"
|
version "2.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1"
|
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1"
|
||||||
@@ -3133,10 +3138,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@rancher/icons/-/icons-2.0.29.tgz#6546d69768c706bebd66bfa73b36aef3d105987a"
|
resolved "https://registry.yarnpkg.com/@rancher/icons/-/icons-2.0.29.tgz#6546d69768c706bebd66bfa73b36aef3d105987a"
|
||||||
integrity sha512-qBBqfazS9y5VjV7fJDPNXmxd9AP/2uiE05mKFWP41kpbO+tEb62RnUBXCm14XLeScDZQcOuiAKVHMmvCFzF0BA==
|
integrity sha512-qBBqfazS9y5VjV7fJDPNXmxd9AP/2uiE05mKFWP41kpbO+tEb62RnUBXCm14XLeScDZQcOuiAKVHMmvCFzF0BA==
|
||||||
|
|
||||||
"@rancher/shell@3.0.5-rc.3":
|
"@rancher/shell@3.0.5-rc.6":
|
||||||
version "3.0.5-rc.3"
|
version "3.0.5-rc.6"
|
||||||
resolved "https://registry.yarnpkg.com/@rancher/shell/-/shell-3.0.5-rc.3.tgz#8dae16e7a9f1ec7c952449d194023ad7eeefdb2f"
|
resolved "https://registry.yarnpkg.com/@rancher/shell/-/shell-3.0.5-rc.6.tgz#8d842cf1b0f7e3e52af53538953cf9dc5bfd0639"
|
||||||
integrity sha512-2EkcS2WECWJz/wEnd2NZFHPomST8wWrJqml1z7rTRkF9vTJMltTOpshZGyAhIBmeNLRYOJKh/VU5l7YInALlcQ==
|
integrity sha512-B9+2WE1a06wRvCN7xvxp/HBdjrib04szIbc1pNBeMdDNCqQmZMvt2dftjYb44Tvg+4Ji8G+tcK8Q3Rry6NDJsw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@aws-sdk/client-ec2" "3.658.1"
|
"@aws-sdk/client-ec2" "3.658.1"
|
||||||
"@aws-sdk/client-eks" "3.1.0"
|
"@aws-sdk/client-eks" "3.1.0"
|
||||||
@@ -3167,6 +3172,7 @@
|
|||||||
babel-plugin-module-resolver "4.0.0"
|
babel-plugin-module-resolver "4.0.0"
|
||||||
babel-preset-vue "2.0.2"
|
babel-preset-vue "2.0.2"
|
||||||
cache-loader "4.1.0"
|
cache-loader "4.1.0"
|
||||||
|
chart.js "4.4.8"
|
||||||
clipboard-polyfill "4.0.1"
|
clipboard-polyfill "4.0.1"
|
||||||
codemirror ">=5.64.0 <6"
|
codemirror ">=5.64.0 <6"
|
||||||
codemirror-editor-vue3 "2.8.0"
|
codemirror-editor-vue3 "2.8.0"
|
||||||
@@ -5761,6 +5767,13 @@ char-regex@^1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
|
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
|
||||||
integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
|
integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
|
||||||
|
|
||||||
|
chart.js@4.4.8:
|
||||||
|
version "4.4.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.8.tgz#54645b638e9d585099bc16b892947b5e6cd2a552"
|
||||||
|
integrity sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==
|
||||||
|
dependencies:
|
||||||
|
"@kurkle/color" "^0.3.0"
|
||||||
|
|
||||||
check-more-types@2.24.0, check-more-types@^2.24.0:
|
check-more-types@2.24.0, check-more-types@^2.24.0:
|
||||||
version "2.24.0"
|
version "2.24.0"
|
||||||
resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
|
resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
|
||||||
|
|||||||
Reference in New Issue
Block a user