Compare commits

..
Author SHA1 Message Date
mergify[bot]andAndy Lee f0014cc627 chore: bump version to v1.6.0-rc2 (#403) (#404)
(cherry picked from commit 193d4536e3)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-16 18:13:20 +08:00
mergify[bot]andAndy Lee 9aa311a7e3 feat: support for subnets and VPCs from UI (#374) (#402)
(cherry picked from commit 15950eb209165a980ec53d22fe2898b94d6b1c8b)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-16 17:48:48 +08:00
mergify[bot]andYiya Chen 013d9f1d7e fix: add default value to prevent exception (#397) (#401)
(cherry picked from commit bcabefe9f3)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-16 15:19:21 +08:00
mergify[bot]andAndy Lee 8c044ba44c ci: add package version check (#396) (#400)
* ci: add package version check



* ci: extract check version in a script



* ci: update env variable



---------


(cherry picked from commit c541f81dc3)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-16 13:21:09 +08:00
mergify[bot]andYiya Chen 7b8592e07d feat: add vm-migration-network setting (#395) (#398)
(cherry picked from commit 4486f71c8f)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-16 10:22:31 +08:00
mergify[bot]andYiya Chen 53d2e67b4f chore: upgrade rancher/shell to 3.0.5-rc6 (#393) (#394)
* chore: bump shell rc6
* chore: add rancher restriction

---------


(cherry picked from commit 060105ead3)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-14 10:01:12 +08:00
mergify[bot]andAndy Lee 1d146a40bd fix: find the correct vmim based on creationTimeStamp (#389) (#390)
(cherry picked from commit 3ea73978ee)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-10 11:22:48 +08:00
mergify[bot]andYiya Chen 11d6943596 feat: add csi-online-expand-validation setting (#378) (#388)
* feat: add csi-online-expand-validation setting
* feat: invalid json error message
* feat: handle API errors
* refactor: remove inStore()

---------


(cherry picked from commit f4e363396d)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-09 15:31:17 +08:00
mergify[bot]andNick Chung 7b754457ec chore: remove run-lint from build-standalone-on-release (#355) (#387)
(cherry picked from commit 7386a2deb6)

Signed-off-by: Nick Chung <nick.chung@suse.com>
Co-authored-by: Nick Chung <Nick.Chung@suse.com>
2025-07-09 14:51:47 +08:00
mergify[bot]andAndy Lee 2cb838eded chore: bump version to v1.6.0-rc1 (#385) (#386)
(cherry picked from commit fa16e24983)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-09 14:36:39 +08:00
mergify[bot]andYiya Chen bdcb34ab14 ci: ignore draft PR add add backport labels (#382) (#384)
* ci: skip draft PR
* ci: fix auto assign
* ci: add backport label
* fix: remove token

---------

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
(cherry picked from commit ee28697161)

# Conflicts:
#	.github/workflows/backport-label.yaml

Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-09 13:28:54 +08:00
mergify[bot]andAndy Lee 8daf44b3f6 fix: read volume accessMode even is RWO (#381) (#383)
(cherry picked from commit ce63ee6a19)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-09 12:07:41 +08:00
mergify[bot]andYiya Chen 8044e74102 feat: disable vm images (#376) (#380)
(cherry picked from commit 85a06feb91)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-08 17:10:14 +08:00
mergify[bot]andNick Chung 264d65cc6d fix: can't migrate network config in Rancher integration (#371) (#373)
(cherry picked from commit 0ef4ff65cc)

Signed-off-by: Nick Chung <nick.chung@suse.com>
Co-authored-by: Nick Chung <Nick.Chung@suse.com>
2025-07-03 16:59:13 +08:00
mergify[bot]andAndy Lee 342a82c8e0 fix: add custom promptRemove for backup and snasphot deletion (#367) (#372)
(cherry picked from commit f12717a8f4)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-03 14:06:40 +08:00
mergify[bot]andNick Chung e7cd5cc4b0 feat: [ENHANCEMENT] Add setting upgrade-config to UI(#8369) (#347) (#363)
(cherry picked from commit dc683a50a4)

Signed-off-by: Nick Chung <nick.chung@suse.com>
Co-authored-by: Nick Chung <Nick.Chung@suse.com>
2025-07-01 16:24:50 +08:00
mergify[bot]andAndy Lee cf405be71e fix: cloned secret should keep originl type (#354) (#362)
(cherry picked from commit 1d89aafeab)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-07-01 11:47:40 +08:00
mergify[bot]andYiya Chen 911d30d7ba feat: SB Enhancements (NS selection and timeout) (#345) (#360)
* feat: add namespace field
* feat: add optional inputs
* feat: refine code
* feat: add feature flag
* refactor: fix lint error
* feat: filter default namespaces
* refactor: hide tips with feature flag
* refactor: use UnitInput
* feat: load default value from settings
* refactor: fix API url
* refactor: no available namespaces
* chore: update subject-case rule
---------
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>

(cherry picked from commit be421054d8)

Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-07-01 09:53:30 +08:00
mergify[bot]andAndy Lee 1b4bfa0962 ci: replace tag with github.event.release.tag_name (#356) (#359)
(cherry picked from commit fcef0391bb)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-06-30 11:01:19 +08:00
Andy Lee d0f6f18a8a docs: update readme.md to include v1.6-head
Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-06-26 11:55:34 +08:00
78 changed files with 2244 additions and 3214 deletions
-1
View File
@@ -14,4 +14,3 @@ dist-pkg
pkg/harvester/index.ts pkg/harvester/index.ts
pkg/harvester/store/* pkg/harvester/store/*
scripts/pkgfile.js scripts/pkgfile.js
tmp
+1
View File
@@ -4,3 +4,4 @@ numberOfReviewers: 0
reviewers: reviewers:
- a110605 - a110605
- houhoucoop - houhoucoop
- NickChungSUSE
+26 -7
View File
@@ -1,14 +1,33 @@
pull_request_rules: pull_request_rules:
- name: Automatically open backport PR to release-harvester-v1.5
conditions:
- base=main
- label="require backport 1.5"
actions:
backport:
branches:
- "release-harvester-v1.5"
assignees:
- "{{ author }}"
labels:
- "v1.5 backport PR"
- name: Automatically open backport PR to release-harvester-v1.0
conditions:
- base=main
- label="require backport 1.0"
actions:
backport:
branches:
- "release-harvester-v1.0"
assignees:
- "{{ author }}"
labels:
- "v1.0 backport PR"
- name: Ask to resolve conflict - name: Ask to resolve conflict
conditions: conditions:
- conflict - conflict
actions: actions:
comment: comment:
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏 message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
commands_restrictions:
backport:
conditions:
- or:
- sender-permission>=write
- sender=github-actions[bot]
-40
View File
@@ -1,40 +0,0 @@
# .github/release.yml
changelog:
categories:
- title: Features
labels:
- feat
- title: Bug Fixes
labels:
- fix
- title: Dependencies
labels:
- deps
- title: Performance
labels:
- perf
- title: Documentation
labels:
- docs
- title: Style
labels:
- style
- title: Build & Continuous Integration
labels:
- build
- ci
- title: Security
labels:
- security
- title: Tests
labels:
- test
- title: Refactoring
labels:
- refactor
- title: Chores
labels:
- chore
- title: Others
labels:
- other
+4 -6
View File
@@ -1,17 +1,15 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": ["config:recommended"],
"config:recommended",
":semanticCommitTypeAll(deps)",
":semanticCommitScopeDisabled"
],
"baseBranches": [ "baseBranches": [
"master",
"main", "main",
"/^release-harvester-v\\d+\\.\\d+$/" "/^release-harvester-v\\d+\\.\\d+$/"
], ],
"automergeMajor": false, "automergeMajor": false,
"semanticCommits": "enabled", "semanticCommits": "enabled",
"semanticCommitType": "deps", "semanticCommitType": "chore",
"semanticCommitScope": "deps",
"prHourlyLimit": 12, "prHourlyLimit": 12,
"timezone": "Asia/Taipei", "timezone": "Asia/Taipei",
"schedule": ["after 10am on sunday"], "schedule": ["after 10am on sunday"],
-49
View File
@@ -1,49 +0,0 @@
name: "[PR Management] Request Backport via Mergify"
on:
pull_request_target:
types: [closed]
branches: [main]
permissions:
pull-requests: write
jobs:
comment-backport:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.base_ref }}
- name: Post Mergify backport command
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
pr_number=${{ github.event.pull_request.number }}
repo="${{ github.repository }}"
labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
labels=$(echo "$labels_json" | jq -r '.[] // empty')
echo "Labels on PR: $labels"
matches=$(echo "$labels" | grep -oE '^require backport/v[0-9]+\.[0-9]+$' || true)
if [[ -z "$matches" ]]; then
echo "No back‑port labels found — skipping."
exit 0
fi
branches=$(echo "$matches" \
| sed -E 's/^require backport\/v/release-harvester-v/' \
| sort -u | tr '\n' ' ')
branches=${branches%% }
cmd="@Mergifyio backport $branches"
echo "Posting Mergify command: $cmd"
gh pr comment "$pr_number" --repo "$repo" --body "$cmd"
@@ -132,7 +132,7 @@ jobs:
git push origin ${{ needs.setup-target-branch.outputs.target_branch }} git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
- name: Run Helm chart releaser - name: Run Helm chart releaser
uses: helm/chart-releaser-action@v1.7.0 uses: helm/chart-releaser-action@v1.4.1
with: with:
charts_dir: ./charts charts_dir: ./charts
env: env:
-30
View File
@@ -1,30 +0,0 @@
name: "[PR Management] Add PR Label"
on:
pull_request_target:
types: [opened, reopened, edited]
branches:
- main
- 'release-harvester-v*'
permissions:
pull-requests: write
jobs:
auto-assign-pr-label:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.base_ref }}
- name: Setup Nodejs and yarn install
uses: ./.github/actions/setup
- name: Set PR label
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
echo "PR_LABEL = $PR_LABEL"
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "$PR_LABEL"
-7
View File
@@ -7,7 +7,6 @@ on:
- main - main
- 'release-*' - 'release-*'
pull_request: pull_request:
types: [opened, reopened, edited, synchronize]
branches: branches:
- main - main
- 'release-*' - 'release-*'
@@ -23,11 +22,6 @@ jobs:
- name: Run tests - name: Run tests
uses: ./.github/actions/lint uses: ./.github/actions/lint
- name: Validate PR title lint
if: github.event_name == 'pull_request'
shell: bash
run: echo "${{ github.event.pull_request.title }}" | npx commitlint
- name: Validate commit messages - name: Validate commit messages
shell: bash shell: bash
run: | run: |
@@ -50,7 +44,6 @@ jobs:
echo "FROM=$FROM" echo "FROM=$FROM"
echo "TO=$TO" echo "TO=$TO"
npx commitlint --from "$FROM" --to "$TO" --verbose npx commitlint --from "$FROM" --to "$TO" --verbose
env: env:
GITHUB_EVENT_NAME: ${{ github.event_name }} GITHUB_EVENT_NAME: ${{ github.event_name }}
+1
View File
@@ -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**
+1 -1
View File
@@ -27,7 +27,7 @@ module.exports = {
'subject-case': [0, 'never'], 'subject-case': [0, 'never'],
'subject-empty': [2, 'never'], 'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'], 'subject-full-stop': [2, 'never', '.'],
'subject-max-length': [0, 'never'], 'subject-max-length': [2, 'always', 72],
'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'],
+10 -11
View File
@@ -1,13 +1,13 @@
{ {
"name": "harvester-ui-extension", "name": "harvester-ui-extension",
"version": "1.7.0-dev", "version": "1.6.0-rc2",
"private": false, "private": false,
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
}, },
"dependencies": { "dependencies": {
"@babel/plugin-transform-class-static-block": "7.28.3", "@babel/plugin-transform-class-static-block": "7.26.0",
"@rancher/shell": "3.0.5-rc.8", "@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",
@@ -16,21 +16,21 @@
"yaml": "^2.5.1" "yaml": "^2.5.1"
}, },
"resolutions": { "resolutions": {
"@types/node": "~20.19.0", "@types/node": "~20.10.0",
"cronstrue": "2.59.0", "cronstrue": "2.50.0",
"d3-color": "3.1.0", "d3-color": "3.1.0",
"ejs": "3.1.10", "ejs": "3.1.10",
"follow-redirects": "1.15.11", "follow-redirects": "1.15.6",
"glob": "7.2.3", "glob": "7.2.3",
"glob-parent": "6.0.2", "glob-parent": "6.0.2",
"json5": "2.2.3", "json5": "2.2.3",
"@types/lodash": "4.17.20", "@types/lodash": "4.17.5",
"merge": "2.1.1", "merge": "2.1.1",
"node-forge": "1.3.1", "node-forge": "1.3.1",
"nth-check": "2.1.1", "nth-check": "2.1.1",
"qs": "6.14.0", "qs": "6.11.1",
"roarr": "7.21.1", "roarr": "7.0.4",
"semver": "7.7.2", "semver": "7.5.4",
"@vue/cli-service/html-webpack-plugin": "^5.0.0" "@vue/cli-service/html-webpack-plugin": "^5.0.0"
}, },
"scripts": { "scripts": {
@@ -46,7 +46,6 @@
"prepare": "husky" "prepare": "husky"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/load": "^19.8.1",
"@commitlint/cli": "^19.8.1", "@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1", "@commitlint/config-conventional": "^19.8.1",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
+2 -5
View File
@@ -11,6 +11,7 @@ const CATEGORY = {
ui: [ ui: [
'branding', 'branding',
'ui-source', 'ui-source',
'ui-plugin-index',
'ui-index', 'ui-index',
] ]
}; };
@@ -97,12 +98,8 @@ export default {
return true; return true;
} }
let description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || ''; const description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || '';
// remove <a> tags from description
if (description.includes('<a')) {
description = description.replace(/<a[^>]*>(.*?)<\/a>/g, '$1');
}
// filter by description // filter by description
if (description.includes(searchQuery)) { if (description.includes(searchQuery)) {
return true; return true;
@@ -257,12 +257,8 @@ export default {
}); });
}, },
reconnect() {
this.$refs.novncConsole.reconnect();
},
softReboot() { softReboot() {
this.vmResource.doSoftReboot(); this.vmResource.softrebootVM();
}, },
showKeysModal() { showKeysModal() {
@@ -310,13 +306,6 @@ export default {
{{ t("harvester.action.softreboot") }} {{ t("harvester.action.softreboot") }}
</button> </button>
<button
class="btn btn-sm bg-primary"
@click="reconnect"
>
{{ t("harvester.action.reconnect") }}
</button>
<v-dropdown <v-dropdown
v-if="!hideCustomKeysBar" v-if="!hideCustomKeysBar"
ref="customKeyPopover" ref="customKeyPopover"
@@ -383,12 +372,3 @@ export default {
background: rgb(40, 40, 40); background: rgb(40, 40, 40);
} }
</style> </style>
<style lang="scss">
.vm-console .v-popper__arrow-container {
display: none;
}
.vm-console .v-popper__popper{
margin-top: 8px;
}
</style>
@@ -1,222 +0,0 @@
<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>
@@ -53,6 +53,8 @@ export default {
}, },
namespaceOptions() { namespaceOptions() {
if (this.availableNamespaces.length === 0) return [];
const allSelected = const allSelected =
this.namespaces.length === this.filteredNamespaces.length && this.namespaces.length === this.filteredNamespaces.length &&
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns)); this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
-2
View File
@@ -7,6 +7,4 @@ export const DOC = {
UPGRADE_CONFIG_URL: `/advanced/index#upgrade-config`, UPGRADE_CONFIG_URL: `/advanced/index#upgrade-config`,
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`, STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`, SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
VPC_CONFIGURATION_EXAMPLES: `/networking/kubeovn-vpc#vpc-peering-configuration-examples`,
NETWORK_POLICY: `/networking/kubeovn-vm-isolation/#network-policies`,
}; };
+2 -9
View File
@@ -39,18 +39,11 @@ const FEATURE_FLAGS = {
], ],
'v1.5.1': [], 'v1.5.1': [],
'v1.6.0': [ 'v1.6.0': [
'vmMachineTypes',
'customSupportBundle', 'customSupportBundle',
'csiOnlineExpandValidation', 'csiOnlineExpandValidation',
'vmNetworkMigration', 'vmNetworkMigration',
'kubeovnVpcSubnet', 'kubeovnVpcSubnet'
'rancherClusterSetting',
'cpuMemoryHotplug',
'cdiSettings',
'vmCloneRunStrategy',
],
'v1.6.1': [],
'v1.7.0': [
'vmMachineTypeAuto'
] ]
}; };
-17
View File
@@ -10,7 +10,6 @@ import {
LOGGING, LOGGING,
STORAGE_CLASS, STORAGE_CLASS,
SECRET, SECRET,
NETWORK_POLICY
} from '@shell/config/types'; } from '@shell/config/types';
import { HCI, VOLUME_SNAPSHOT } from '../types'; import { HCI, VOLUME_SNAPSHOT } from '../types';
import { import {
@@ -426,7 +425,6 @@ export function init($plugin, store) {
HCI.CLUSTER_NETWORK, HCI.CLUSTER_NETWORK,
HCI.NETWORK_ATTACHMENT, HCI.NETWORK_ATTACHMENT,
HCI.VPC, HCI.VPC,
NETWORK_POLICY,
HCI.LB, HCI.LB,
HCI.IP_POOL, HCI.IP_POOL,
], ],
@@ -568,21 +566,6 @@ export function init($plugin, store) {
ifHaveType: HCI.VPC, ifHaveType: HCI.VPC,
}); });
configureType(NETWORK_POLICY, { hiddenNamespaceGroupButton: true, canYaml: false });
virtualType({
labelKey: 'harvester.networkPolicy.label',
name: NETWORK_POLICY,
namespaced: true,
weight: 186,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: NETWORK_POLICY }
},
exact: false,
ifHaveType: NETWORK_POLICY,
});
configureType(HCI.SNAPSHOT, { configureType(HCI.SNAPSHOT, {
isCreatable: false, isCreatable: false,
location: { location: {
-13
View File
@@ -1,5 +1,3 @@
import { HCI } from '../types';
// TODO: delete this not used variable // TODO: delete this not used variable
export const MemoryUnit = [{ export const MemoryUnit = [{
label: 'Mi', label: 'Mi',
@@ -83,14 +81,3 @@ export const CSI_SECRETS = {
CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name', CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name',
CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace', CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace',
}; };
// Some harvester CRD type is not equal to model file name, define the mapping here
export const HARVESTER_CRD_MAP = {
node: HCI.HOST,
configmap: HCI.CLOUD_TEMPLATE,
persistentvolumeclaim: HCI.VOLUME,
'snapshot.storage.k8s.io.volumesnapshot': HCI.SNAPSHOT,
// specific groupable table detail page
'network.harvesterhci.io.vlanconfig': HCI.CLUSTER_NETWORK,
'kubeovn.io.subnet': HCI.VPC
};
+1 -6
View File
@@ -70,10 +70,5 @@ export const HCI = {
K8S_ARCH: 'kubernetes.io/arch', K8S_ARCH: 'kubernetes.io/arch',
IMAGE_DISPLAY_NAME: 'harvesterhci.io/imageDisplayName', IMAGE_DISPLAY_NAME: 'harvesterhci.io/imageDisplayName',
CUSTOM_IP: 'harvesterhci.io/custom-ip', CUSTOM_IP: 'harvesterhci.io/custom-ip',
IMPORTED_IMAGE: 'migration.harvesterhci.io/imported', IMPORTED_IMAGE: 'migration.harvesterhci.io/imported'
VM_CPU_MEMORY_HOTPLUG: 'harvesterhci.io/enableCPUAndMemoryHotplug',
FILESYSTEM_OVERHEAD: 'cdi.harvesterhci.io/filesystemOverhead',
CLONE_STRATEGY: 'cdi.harvesterhci.io/storageProfileCloneStrategy',
VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes',
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
}; };
+2 -6
View File
@@ -4,6 +4,7 @@ export const HCI_SETTING = {
LOG_LEVEL: 'log-level', LOG_LEVEL: 'log-level',
SERVER_VERSION: 'server-version', SERVER_VERSION: 'server-version',
UI_INDEX: 'ui-index', UI_INDEX: 'ui-index',
UI_PLUGIN_INDEX: 'ui-plugin-index',
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled', UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
UPGRADE_CHECKER_URL: 'upgrade-checker-url', UPGRADE_CHECKER_URL: 'upgrade-checker-url',
VLAN: 'vlan', VLAN: 'vlan',
@@ -36,8 +37,6 @@ export const HCI_SETTING = {
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio', ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
UPGRADE_CONFIG: 'upgrade-config', UPGRADE_CONFIG: 'upgrade-config',
VM_MIGRATION_NETWORK: 'vm-migration-network', VM_MIGRATION_NETWORK: 'vm-migration-network',
RANCHER_CLUSTER: 'rancher-cluster',
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio'
}; };
export const HCI_ALLOWED_SETTINGS = { export const HCI_ALLOWED_SETTINGS = {
@@ -83,6 +82,7 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES]: { from: 'import', canReset: true }, [HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES]: { from: 'import', canReset: true },
[HCI_SETTING.AUTO_DISK_PROVISION_PATHS]: { canReset: true }, [HCI_SETTING.AUTO_DISK_PROVISION_PATHS]: { canReset: true },
[HCI_SETTING.RELEASE_DOWNLOAD_URL]: { kind: 'url' }, [HCI_SETTING.RELEASE_DOWNLOAD_URL]: { kind: 'url' },
[HCI_SETTING.UI_PLUGIN_INDEX]: { kind: 'url' },
[HCI_SETTING.CONTAINERD_REGISTRY]: { [HCI_SETTING.CONTAINERD_REGISTRY]: {
kind: 'json', from: 'import', canReset: true kind: 'json', from: 'import', canReset: true
}, },
@@ -108,10 +108,6 @@ export const HCI_ALLOWED_SETTINGS = {
featureFlag: 'upgradeConfigSetting', featureFlag: 'upgradeConfigSetting',
docPath: 'UPGRADE_CONFIG_URL' docPath: 'UPGRADE_CONFIG_URL'
}, },
[HCI_SETTING.RANCHER_CLUSTER]: {
kind: 'custom', from: 'import', canReset: true, featureFlag: 'rancherClusterSetting'
},
[HCI_SETTING.MAX_HOTPLUG_RATIO]: { kind: 'number', featureFlag: 'cpuMemoryHotplug' },
[HCI_SETTING.VM_MIGRATION_NETWORK]: { [HCI_SETTING.VM_MIGRATION_NETWORK]: {
kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration', kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration',
}, },
-5
View File
@@ -18,8 +18,3 @@ export const NETWORK_PROTOCOL = {
IPv4: 'IPv4', IPv4: 'IPv4',
IPv6: 'IPv6', IPv6: 'IPv6',
}; };
export const INTERNAL_STORAGE_CLASS = {
VMSTATE_PERSISTENCE: 'vmstate-persistence',
LONGHORN_STATIC: 'longhorn-static',
};
@@ -47,26 +47,20 @@ export default {
STATE, STATE,
NAME, NAME,
{ {
name: 'CPU', name: 'vmCPU',
label: 'CPU', labelKey: 'tableHeaders.cpu',
sort: ['displayCPU'], search: false,
value: 'displayCPU', sort: ['spec.template.spec.domain.cpu.cores'],
align: 'center', value: 'spec.template.spec.domain.cpu.cores',
dashIfEmpty: true, width: 120
}, },
{ {
name: 'Memory', name: 'vmRAM',
value: 'displayMemory', labelKey: 'glance.memory',
search: false,
sort: ['memorySort'], sort: ['memorySort'],
align: 'center', value: 'spec.template.spec.domain.resources.limits.memory',
labelKey: 'tableHeaders.memory', width: 120
formatter: 'Si',
formatterOpts: {
opts: {
increment: 1024, addSuffix: true, maxExponent: 3, minExponent: 3, suffix: 'i',
},
needParseSi: true
},
}, },
{ {
name: 'ip', name: 'ip',
@@ -172,9 +172,6 @@ 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,9 +172,6 @@ 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">
@@ -5,7 +5,6 @@ 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';
@@ -92,9 +91,9 @@ export default {
}, },
flavor() { flavor() {
const { cpu, memory } = getVmCPUMemoryValues(this.value); const domain = this.value?.spec?.template?.spec?.domain;
return `${ cpu } vCPU , ${ memory } ${ this.t('harvester.virtualMachine.input.memory') }`; return `${ domain.cpu?.cores } vCPU , ${ domain.resources?.limits?.memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
}, },
kernelRelease() { kernelRelease() {
+4 -18
View File
@@ -1,13 +1,12 @@
<script> <script>
import { exceptionToErrorsArray } from '@shell/utils/error'; import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { runStrategies as runStrategyOptions } from '../config/harvester-map';
import { Card } from '@components/Card'; import { Card } from '@components/Card';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import { Checkbox } from '@components/Form/Checkbox'; import { Checkbox } from '@components/Form/Checkbox';
import AsyncButton from '@shell/components/AsyncButton'; import AsyncButton from '@shell/components/AsyncButton';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default { export default {
name: 'CloneVMModal', name: 'CloneVMModal',
@@ -15,7 +14,7 @@ export default {
emits: ['close'], emits: ['close'],
components: { components: {
AsyncButton, Banner, Checkbox, Card, LabeledInput, LabeledSelect AsyncButton, Banner, Checkbox, Card, LabeledInput
}, },
props: { props: {
@@ -29,9 +28,7 @@ export default {
return { return {
name: '', name: '',
cloneData: true, cloneData: true,
errors: [], errors: []
runStrategy: runStrategyOptions[1],
runStrategyOptions
}; };
}, },
@@ -41,9 +38,6 @@ export default {
actionResource() { actionResource() {
return this.resources[0]; return this.resources[0];
}, },
vmCloneRunStrategyEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('vmCloneRunStrategy');
},
}, },
methods: { methods: {
@@ -64,8 +58,7 @@ export default {
// deep clone // deep clone
try { try {
const payload = this.vmCloneRunStrategyEnabled ? { targetVm: this.name, runStrategy: this.runStrategy } : { targetVm: this.name }; const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
const res = await this.actionResource.doAction('clone', payload, {}, false);
if (res._status === 200 || res._status === 204) { if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', { this.$store.dispatch('growl/success', {
@@ -113,13 +106,6 @@ export default {
:label="t('harvester.modal.cloneVM.name')" :label="t('harvester.modal.cloneVM.name')"
required required
/> />
<LabeledSelect
v-if="vmCloneRunStrategyEnabled"
v-model:value="runStrategy"
label-key="harvester.virtualMachine.runStrategy"
:options="runStrategyOptions"
:mode="mode"
/>
<Banner <Banner
v-for="(err, i) in errors" v-for="(err, i) in errors"
:key="i" :key="i"
+149
View File
@@ -0,0 +1,149 @@
<script>
import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import { Checkbox } from '@components/Form/Checkbox';
import AsyncButton from '@shell/components/AsyncButton';
import { LabeledInput } from '@components/Form/LabeledInput';
export default {
name: 'CloneVMModal',
emits: ['close'],
components: {
AsyncButton, Banner, Checkbox, Card, LabeledInput
},
props: {
resources: {
type: Array,
required: true
}
},
data() {
return {
name: '',
cloneData: true,
errors: []
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0];
},
},
methods: {
close() {
this.name = '';
this.$emit('close');
},
async create(buttonCb) {
// shadow clone
if (!this.cloneData) {
this.resources[0].goToClone();
buttonCb(false);
this.close();
return;
}
// deep clone
try {
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', {
title: this.t('harvester.notification.title.succeed'),
message: this.t('harvester.modal.cloneVM.message.success', { name: this.name })
}, { root: true });
this.close();
buttonCb(true);
} else {
const error = [res?.data] || exceptionToErrorsArray(res);
this['errors'] = error;
buttonCb(false);
}
} catch (err) {
const error = err?.data || err;
const message = exceptionToErrorsArray(error);
this['errors'] = message;
buttonCb(false);
}
}
},
};
</script>
<template>
<Card :show-highlight-border="false">
<template #title>
{{ t('harvester.modal.cloneVM.title') }}
</template>
<template #body>
<Checkbox
v-model:value="cloneData"
class="mb-10"
label-key="harvester.modal.cloneVM.type"
/>
<LabeledInput
v-show="cloneData"
v-model:value="name"
class="mb-20"
:label="t('harvester.modal.cloneVM.name')"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
:label="err"
color="error"
/>
</template>
<template #actions>
<div class="actions">
<div class="buttons">
<button
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="create"
:action-label="cloneData ? t('harvester.modal.cloneVM.action.create') : t('harvester.modal.cloneVM.action.clone')"
:disabled="cloneData && !name"
@click="create"
/>
</div>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -1,185 +0,0 @@
<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,7 +10,6 @@ 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',
@@ -106,17 +105,11 @@ export default {
} }
const options = storages.filter((s) => !s.parameters?.backingImage).map((s) => { const options = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name; const 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,
}; };
}) || []; }) || [];
@@ -7,8 +7,6 @@ import { Checkbox } from '@components/Form/Checkbox';
import { HCI } from '../../types'; import { HCI } from '../../types';
import { DOC } from '../../config/doc-links'; import { DOC } from '../../config/doc-links';
import { docLink } from '../../utils/feature-flags'; import { docLink } from '../../utils/feature-flags';
import isEqual from 'lodash/isEqual';
import { clone } from '@shell/utils/object';
export const ksmtunedMode = [{ export const ksmtunedMode = [{
value: 'standard', value: 'standard',
@@ -66,13 +64,11 @@ export default {
this.enableMergeAcrossNodes = !!this.ksmtuned.spec?.mergeAcrossNodes; this.enableMergeAcrossNodes = !!this.ksmtuned.spec?.mergeAcrossNodes;
this.spec = this.ksmtuned.spec; this.spec = this.ksmtuned.spec;
this.originSpec = clone(this.ksmtuned.spec);
}, },
data() { data() {
return { return {
ksmtuned: {}, ksmtuned: {},
originSpec: {},
spec: {}, spec: {},
thresCoef: 30, thresCoef: 30,
ksmtunedMode, ksmtunedMode,
@@ -103,11 +99,6 @@ export default {
methods: { methods: {
async saveKsmtuned() { async saveKsmtuned() {
// no ksmtuned spec change, skip save
if (isEqual(this.spec, this.originSpec)) {
return;
}
this.spec.mergeAcrossNodes = this.enableMergeAcrossNodes ? 1 : 0; this.spec.mergeAcrossNodes = this.enableMergeAcrossNodes ? 1 : 0;
this.ksmtuned['spec'] = this.spec; this.ksmtuned['spec'] = this.spec;
@@ -527,6 +527,7 @@ export default {
id: this.longhornNode.id, id: this.longhornNode.id,
opt: { force: true }, opt: { force: true },
}); });
await new Promise((resolve) => setTimeout(resolve, '5000')); await new Promise((resolve) => setTimeout(resolve, '5000'));
await retrySave(); await retrySave();
} else { } else {
@@ -15,7 +15,6 @@ import { HCI } from '../types';
import { sortBy } from '@shell/utils/sort'; import { sortBy } from '@shell/utils/sort';
import { BACKUP_TYPE } from '../config/types'; import { BACKUP_TYPE } from '../config/types';
import { _EDIT, _CREATE } from '@shell/config/query-params'; import { _EDIT, _CREATE } from '@shell/config/query-params';
import { isBackupTargetSettingEmpty, isBackupTargetSettingUnavailable } from '../utils/setting';
export default { export default {
name: 'CreateVMSchedule', name: 'CreateVMSchedule',
@@ -94,7 +93,7 @@ export default {
return this.settings.find( (O) => O.id === 'backup-target'); return this.settings.find( (O) => O.id === 'backup-target');
}, },
isEmptyValue() { isEmptyValue() {
return isBackupTargetSettingEmpty(this.backupTargetResource); return this.getBackupTargetValueIsEmpty(this.backupTargetResource);
}, },
canUpdate() { canUpdate() {
return this?.backupTargetResource?.canUpdate; return this?.backupTargetResource?.canUpdate;
@@ -110,7 +109,7 @@ export default {
!!this.value.spec.maxFailure; !!this.value.spec.maxFailure;
}, },
isBackupTargetUnAvailable() { isBackupTargetUnAvailable() {
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && isBackupTargetSettingUnavailable(this.backupTargetResource); return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && (this.errorMessage || this.isEmptyValue) && this.canUpdate;
}, },
vmOptions() { vmOptions() {
const nsVmList = this.$store.getters['harvester/all'](HCI.VM).filter((vm) => vm.metadata.namespace === this.value.metadata.namespace); const nsVmList = this.$store.getters['harvester/all'](HCI.VM).filter((vm) => vm.metadata.namespace === this.value.metadata.namespace);
@@ -167,6 +166,20 @@ export default {
onTypeChange(newType) { onTypeChange(newType) {
this.value.metadata.name = `svm${ newType }-${ this.value.spec.vmbackup.source.name }`; this.value.metadata.name = `svm${ newType }-${ this.value.spec.vmbackup.source.name }`;
}, },
getBackupTargetValueIsEmpty(resource) {
let out = true;
if (resource?.value) {
try {
const valueJson = JSON.parse(resource?.value);
out = !valueJson.type;
} catch (e) {}
}
return out;
},
validateFailure(count) { validateFailure(count) {
if (this.value.spec.retain && count > this.value.spec.retain) { if (this.value.spec.retain && count > this.value.spec.retain) {
this.value.spec['maxFailure'] = this.value.spec.retain; this.value.spec['maxFailure'] = this.value.spec.retain;
@@ -1,319 +0,0 @@
<script>
import { VOLUME_MODE } from '@pkg/harvester/config/types';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import ArrayList from '@shell/components/form/ArrayList';
import { Checkbox } from '@components/Form/Checkbox';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { VOLUME_SNAPSHOT_CLASS, HCI } from '../../types';
import { HCI_SETTING } from '../../config/settings';
import { allHash } from '@shell/utils/promise';
import { _EDIT, _CREATE } from '@shell/config/query-params';
export default {
name: 'CDISettings',
props: {
value: {
type: Object,
required: true
},
mode: {
type: String,
required: true
},
provisioner: {
type: String,
required: true
},
},
components: {
ArrayList,
Checkbox,
LabeledInput,
LabeledSelect,
},
emits: ['update:cdiSettings'],
async fetch() {
const hash = {
volumeSnapshotClasses: this.$store.dispatch(`${ this.inStore }/findAll`, { type: VOLUME_SNAPSHOT_CLASS }),
csiDriverConfigSetting: this.$store.dispatch(`${ this.inStore }/find`, { type: HCI.SETTING, id: HCI_SETTING.CSI_DRIVER_CONFIG }),
};
await allHash(hash);
if (this.mode === _CREATE ) {
this.setDefaultVolumeSnapshotClass();
} else {
this.initCDISettingsFromAnnotations();
}
},
data() {
return {
cdiSettings: {
volumeModeAccessModes: [],
volumeSnapshotClass: null,
cloneStrategy: null,
filesystemOverhead: null,
},
defaultAddValue: { volumeMode: null, accessModes: [] },
noneOption: { label: 'None', value: '' },
};
},
computed: {
inStore() {
return this.$store.getters['currentProduct'].inStore;
},
allVolumeModeOptions() {
return Object.values(VOLUME_MODE).map((mode) => ({ label: mode, value: mode }));
},
selectedVolumeModes() {
return this.cdiSettings.volumeModeAccessModes
.map((item) => item.volumeMode)
.filter(Boolean);
},
volumeModeOptions() {
return [this.noneOption, ...this.allVolumeModeOptions].filter( (option) => !this.selectedVolumeModes.includes(option.value) );
},
allVolumeModesSelected() {
return this.selectedVolumeModes.length === this.allVolumeModeOptions.length;
},
accessModeOptions() {
return [
{ label: 'ReadWriteOnce', value: 'ReadWriteOnce' },
{ label: 'ReadOnlyMany', value: 'ReadOnlyMany' },
{ label: 'ReadWriteMany', value: 'ReadWriteMany' },
{ label: 'ReadWriteOncePod', value: 'ReadWriteOncePod' },
];
},
cloneStrategyOptions() {
return [
this.noneOption,
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.copy'), value: 'copy' },
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.snapshot'), value: 'snapshot' },
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.csiClone'), value: 'csi-clone' },
];
},
volumeSnapshotClassOptions() {
const allClasses = this.$store.getters[`${ this.inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
const filtered = allClasses.filter((c) => c.driver === this.provisioner);
return [this.noneOption, ...filtered.map((c) => ({ label: c.name, value: c.name }))];
},
isFilesystemOverheadValid() {
const val = this.cdiSettings.filesystemOverhead;
if (val === null || val === '') return true;
const regex = /^(0(\.\d{1,3})?|1(\.0{1,3})?)$/;
return regex.test(val);
},
isCustomClass() {
return this.mode === _CREATE || this.mode === _EDIT;
}
},
watch: {
provisioner() {
this.resetCdiSettings();
this.$nextTick(this.setDefaultVolumeSnapshotClass);
},
cdiSettings: {
handler(val) {
this.$emit('update:cdiSettings', val);
},
deep: true,
immediate: true,
}
},
methods: {
setDefaultVolumeSnapshotClass() {
try {
const setting = this.$store.getters[`${ this.inStore }/byId`](HCI.SETTING, HCI_SETTING.CSI_DRIVER_CONFIG);
const config = JSON.parse(setting?.value || setting?.default || '{}');
const defaultClass = config?.[this.provisioner]?.volumeSnapshotClassName || null;
const allClasses = this.$store.getters[`${ this.inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
const matched = allClasses.find((cls) => cls.name === defaultClass && cls.driver === this.provisioner);
this.cdiSettings.volumeSnapshotClass = matched?.name || null;
} catch (e) {
console.error('Failed to parse CSI config:', e); // eslint-disable-line no-console
this.cdiSettings.volumeSnapshotClass = null;
}
},
initCDISettingsFromAnnotations() {
const annotations = this.value?.metadata?.annotations || {};
let volumeModeAccessModes = [];
const rawVolumeMode = annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES];
if (rawVolumeMode) {
try {
const parsed = JSON.parse(rawVolumeMode);
volumeModeAccessModes = Object.entries(parsed).map(([volumeMode, accessModes]) => ({
volumeMode,
accessModes: Array.isArray(accessModes) ? accessModes : [],
}));
} catch (e) {
console.error('Failed to parse annotation:', e); // eslint-disable-line no-console
}
}
if (volumeModeAccessModes.length) {
this.cdiSettings.volumeModeAccessModes = volumeModeAccessModes;
}
if (annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS]) {
this.cdiSettings.volumeSnapshotClass = annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS];
}
if (annotations[HCI_ANNOTATIONS.CLONE_STRATEGY]) {
this.cdiSettings.cloneStrategy = annotations[HCI_ANNOTATIONS.CLONE_STRATEGY];
}
if (annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD]) {
this.cdiSettings.filesystemOverhead = annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD];
}
},
resetCdiSettings() {
this.cdiSettings.volumeModeAccessModes = [this.defaultAddValue];
this.cdiSettings.volumeSnapshotClass = null;
this.cdiSettings.cloneStrategy = null;
this.cdiSettings.filesystemOverhead = null;
},
validateFilesystemOverhead(e) {
const val = e.target.value;
this.cdiSettings.filesystemOverhead = val;
}
}
};
</script>
<template>
<ArrayList
v-model:value="cdiSettings.volumeModeAccessModes"
:initial-empty-row="true"
:show-header="true"
:mode="mode"
:title="t('harvester.storage.cdiSettings.volumeModeAccessModes.label')"
:add-label="t('harvester.storage.cdiSettings.volumeModeAccessModes.add')"
:default-add-value="defaultAddValue"
:protip="t('harvester.storage.cdiSettings.volumeModeAccessModes.tooltip')"
:add-disabled="allVolumeModesSelected"
>
<template #column-headers>
<div class="column-headers">
<div
class="row"
:class="{ custom: isCustomClass }"
>
<label
class="col span-3 value text-label mb-10"
for="volumeMode"
>
{{ t('harvester.storage.cdiSettings.volumeModeAccessModes.volumeMode') }}
</label>
<label
class="col span-9 value text-label mb-10"
for="accessModes"
>
{{ t('harvester.storage.cdiSettings.volumeModeAccessModes.accessModes.label') }}
</label>
</div>
</div>
</template>
<template #columns="{ row }">
<div class="row">
<div class="col span-3">
<LabeledSelect
id="volumeMode"
v-model:value="row.value.volumeMode"
:mode="mode"
:options="volumeModeOptions"
/>
</div>
<div
id="accessModes"
class="col span-9"
>
<Checkbox
v-for="opt in accessModeOptions"
:key="opt.value"
v-model:value="row.value.accessModes"
:value-when-true="opt.value"
:label="opt.label"
type="checkbox"
:mode="mode"
/>
</div>
</div>
</template>
</ArrayList>
<LabeledSelect
v-model:value="cdiSettings.volumeSnapshotClass"
class="select mt-20 mb-20"
:label="t('harvester.storage.cdiSettings.volumeSnapshotClass.label')"
:tooltip="t('harvester.storage.cdiSettings.volumeSnapshotClass.tooltip')"
:mode="mode"
:options="volumeSnapshotClassOptions"
/>
<LabeledSelect
v-model:value="cdiSettings.cloneStrategy"
class="select mb-20"
:label="t('harvester.storage.cdiSettings.cloneStrategy.label')"
:tooltip="t('harvester.storage.cdiSettings.cloneStrategy.tooltip')"
:mode="mode"
:options="cloneStrategyOptions"
/>
<LabeledInput
v-model:value="cdiSettings.filesystemOverhead"
class="select mb-20"
:label="t('harvester.storage.cdiSettings.fileSystemOverhead.label')"
:tooltip="t('harvester.storage.cdiSettings.fileSystemOverhead.tooltip')"
:mode="mode"
type="number"
:min="0"
:max="1"
:step="0.001"
:placeholder="t('harvester.storage.cdiSettings.fileSystemOverhead.placeholder')"
:status="isFilesystemOverheadValid ? null : 'error'"
@input="validateFilesystemOverhead"
/>
</template>
<style scoped lang="scss">
.column-headers .row.custom {
max-width: calc(100% - 75px);
}
.row {
align-items: center;
}
.select {
max-width: 480px;
}
</style>
@@ -1,5 +1,4 @@
<script> <script>
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import CruResource from '@shell/components/CruResource'; import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription'; import NameNsDescription from '@shell/components/form/NameNsDescription';
@@ -7,9 +6,11 @@ import ArrayList from '@shell/components/form/ArrayList';
import Tab from '@shell/components/Tabbed/Tab'; import Tab from '@shell/components/Tabbed/Tab';
import Tabbed from '@shell/components/Tabbed'; import Tabbed from '@shell/components/Tabbed';
import { RadioGroup } from '@components/Form/Radio'; import { RadioGroup } from '@components/Form/Radio';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import Loading from '@shell/components/Loading'; import Loading from '@shell/components/Loading';
import { _CREATE, _VIEW } from '@shell/config/query-params'; import { _CREATE, _VIEW } from '@shell/config/query-params';
import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features'; import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
import { import {
@@ -21,7 +22,6 @@ import { CSI_DRIVER } from '../../types';
import Tags from '../../components/DiskTags'; import Tags from '../../components/DiskTags';
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim'; import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass'; import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
import CDISettings from './CDISettings';
export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node'; export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node';
@@ -45,7 +45,6 @@ export default {
Tabbed, Tabbed,
Loading, Loading,
Tags, Tags,
CDISettings,
}, },
mixins: [CreateEditView], mixins: [CreateEditView],
@@ -118,7 +117,6 @@ export default {
key: '', key: '',
values: [], values: [],
}, },
cdiSettings: {},
}; };
}, },
@@ -129,7 +127,7 @@ export default {
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }), namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }), storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }), longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }), csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER })
}; };
if (this.value.longhornV2LVMSupport) { if (this.value.longhornV2LVMSupport) {
@@ -199,14 +197,6 @@ export default {
return v2DataEngine.value === 'true' ? DATA_ENGINE_V2 : DATA_ENGINE_V1; return v2DataEngine.value === 'true' ? DATA_ENGINE_V2 : DATA_ENGINE_V1;
}, },
isCDISettingsFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('cdiSettings');
},
shouldShowCDISettingsTab() {
return this.isCDISettingsFeatureEnabled && this.provisioner !== `${ LONGHORN_DRIVER }_${ DATA_ENGINE_V1 }`;
},
}, },
watch: { watch: {
@@ -259,10 +249,6 @@ export default {
}); });
this.formatAllowedTopoloties(); this.formatAllowedTopoloties();
if (this.shouldShowCDISettingsTab) {
this.formatCDISettings();
}
}, },
formatAllowedTopoloties() { formatAllowedTopoloties() {
@@ -284,42 +270,6 @@ export default {
if (matchLabelExpressions.length > 0) { if (matchLabelExpressions.length > 0) {
this.value.allowedTopologies = [{ matchLabelExpressions: [...matchLabelExpressions, ...lvmMatchExpression] }]; this.value.allowedTopologies = [{ matchLabelExpressions: [...matchLabelExpressions, ...lvmMatchExpression] }];
} }
},
formatCDISettings() {
const annotations = this.value.metadata.annotations || {};
const volumeModeAccessModes = {};
this.cdiSettings.volumeModeAccessModes.forEach((setting) => {
if (setting.volumeMode && Array.isArray(setting.accessModes) && setting.accessModes.length > 0) {
volumeModeAccessModes[setting.volumeMode] = setting.accessModes;
}
});
if (Object.keys(volumeModeAccessModes).length > 0) {
annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES] = JSON.stringify(volumeModeAccessModes);
} else {
delete annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES];
}
if (this.cdiSettings.volumeSnapshotClass) {
annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS] = this.cdiSettings.volumeSnapshotClass;
} else {
delete annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS];
}
if (this.cdiSettings.cloneStrategy) {
annotations[HCI_ANNOTATIONS.CLONE_STRATEGY] = this.cdiSettings.cloneStrategy;
} else {
delete annotations[HCI_ANNOTATIONS.CLONE_STRATEGY];
}
if (this.cdiSettings.filesystemOverhead) {
annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD] = this.cdiSettings.filesystemOverhead;
} else {
delete annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD];
}
this.value.metadata.annotations = annotations;
} }
} }
}; };
@@ -454,19 +404,6 @@ export default {
</template> </template>
</ArrayList> </ArrayList>
</Tab> </Tab>
<Tab
v-if="shouldShowCDISettingsTab"
name="cdiSettings"
:label="t('harvester.storage.cdiSettings.label')"
:weight="-2"
>
<CDISettings
v-model:cdi-settings="cdiSettings"
:value="value"
:mode="mode"
:provisioner="value.provisioner"
/>
</Tab>
</Tabbed> </Tabbed>
</CruResource> </CruResource>
</template> </template>
@@ -17,7 +17,6 @@ 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';
@@ -169,18 +168,11 @@ export default {
return filteredStorages return filteredStorages
.map((s) => { .map((s) => {
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name; const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
let disabled = false;
if (isInternalStorageClass(s.name)) {
label += ` (${ this.t('harvester.storage.internal.label') })`;
disabled = true;
}
return { return {
label, label,
value: s.name, value: s.name,
disabled
}; };
}) || []; }) || [];
}, },
@@ -257,12 +257,8 @@ export default {
:label="t('harvester.vmTemplate.tabs.basics')" :label="t('harvester.vmTemplate.tabs.basics')"
> >
<CpuMemory <CpuMemory
class="mb-20"
:cpu="cpu" :cpu="cpu"
:memory="memory" :memory="memory"
:max-cpu="maxCpu"
:max-memory="maxMemory"
:enable-hot-plug="cpuMemoryHotplugEnabled"
:disabled="isConfig" :disabled="isConfig"
@updateCpuMemory="updateCpuMemory" @updateCpuMemory="updateCpuMemory"
/> />
@@ -23,7 +23,6 @@ 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',
@@ -207,18 +206,11 @@ export default {
storageClassOptions() { storageClassOptions() {
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => { return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name; const 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
}; };
}) || []; }) || [];
}, },
@@ -1,196 +0,0 @@
<script>
import debounce from 'lodash/debounce';
import { _EDIT, _VIEW } from '@shell/config/query-params';
import { removeAt } from '@shell/utils/array';
import { Banner } from '@components/Banner';
import InfoBox from '@shell/components/InfoBox';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
export default {
name: 'AccessControlList',
emits: ['update:value'],
components: {
Banner, InfoBox, LabeledSelect, LabeledInput
},
props: {
value: {
type: Array,
default: null,
},
mode: {
type: String,
default: _EDIT,
},
},
data() {
const rows = (this.value || []).map((row) => {
return {
action: row.action || '',
direction: row.direction || '',
priority: row.priority || 0,
match: row.match || '',
};
});
return { rows };
},
computed: {
isView() {
return this.mode === _VIEW;
},
actionOptions() {
return [
{ label: 'Allow', value: 'allow' },
{ label: 'Drop', value: 'drop' },
{ label: 'Pass', value: 'pass' },
{ label: 'Reject', value: 'reject' },
{ label: 'Allow-related', value: 'allow-related' },
{ label: 'Allow-stateless', value: 'allow-stateless' },
];
},
directionOptions() {
return [
{ label: 'To-lport', value: 'to-lport' },
{ label: 'From-lport', value: 'from-lport' },
];
}
},
created() {
this.queueUpdate = debounce(this.update, 100);
},
methods: {
add() {
this.rows.push({
action: '',
direction: '',
priority: this.rows.length,
match: '',
});
this.queueUpdate();
},
removeRule(idx) {
removeAt(this.rows, idx);
this.queueUpdate();
},
update() {
if (this.isView) {
return;
}
this.$emit('update:value', this.rows);
}
},
};
</script>
<template>
<div>
<Banner
v-if="rows.length > 0"
color="info"
>
<t
k="harvester.subnet.acl.banner"
:raw="true"
/>
</Banner>
<div
v-for="(row, idx) in rows"
:key="idx"
>
<InfoBox class="box">
<button
type="button"
class="role-link btn btn-sm removeBtn"
:disabled="isView"
@click="removeRule(idx)"
>
<i class="icon icon-x" />
</button>
<div class="row">
<div class="col span-4">
<LabeledSelect
v-model:value="row.action"
:mode="mode"
class="mt-5 ml-5"
:options="actionOptions"
:required="true"
:label="t('harvester.subnet.acl.action.label')"
:placeholder="t('harvester.subnet.acl.action.placeholder')"
@update:value="update"
/>
</div>
<div class="col span-4">
<LabeledSelect
v-model:value="row.direction"
:mode="mode"
:options="directionOptions"
class="mt-5"
:required="true"
:label="t('harvester.subnet.acl.direction.label')"
:placeholder="t('harvester.subnet.acl.direction.placeholder')"
@update:value="update"
/>
</div>
<div class="col span-3">
<LabeledInput
v-model:value.number="row.priority"
type="number"
class="mb-20 mt-5"
:max="32767"
:min="0"
:mode="mode"
required
label-key="harvester.subnet.acl.priority.label"
@update:value="update"
/>
</div>
</div>
<div class="row">
<div class="col span-11">
<LabeledInput
v-model:value="row.match"
class="mb-5 ml-5"
:mode="mode"
required
:placeholder="t('harvester.subnet.acl.match.placeholder')"
label-key="harvester.subnet.acl.match.label"
@update:value="update"
/>
</div>
</div>
</InfoBox>
</div>
<button
type="button"
class="btn role-tertiary add"
:disabled="isView"
@click="add()"
>
<t k="harvester.subnet.acl.addRule" />
</button>
</div>
</template>
<style lang="scss" scoped>
.box {
position: relative;
}
.removeBtn {
position: absolute;
top: 10px;
right: 10px;
padding: 0px;
}
</style>
+2 -78
View File
@@ -14,8 +14,6 @@ import ArrayList from '@shell/components/form/ArrayList';
import { allHash } from '@shell/utils/promise'; import { allHash } from '@shell/utils/promise';
import { HCI } from '../../types'; import { HCI } from '../../types';
import ResourceTabs from '@shell/components/form/ResourceTabs/index'; import ResourceTabs from '@shell/components/form/ResourceTabs/index';
import { Banner } from '@components/Banner';
import AccessControlList from './AccessControlList';
export default { export default {
name: 'EditSubnet', name: 'EditSubnet',
@@ -23,7 +21,6 @@ export default {
emits: ['update:value'], emits: ['update:value'],
components: { components: {
Banner,
CruResource, CruResource,
LabeledInput, LabeledInput,
LabeledSelect, LabeledSelect,
@@ -33,7 +30,6 @@ export default {
ArrayList, ArrayList,
ResourceTabs, ResourceTabs,
Loading, Loading,
AccessControlList
}, },
mixins: [CreateEditView], mixins: [CreateEditView],
@@ -42,9 +38,7 @@ export default {
created() { created() {
const vpc = this.$route.query.vpc || ''; const vpc = this.$route.query.vpc || '';
const enableDHCP = this.value?.spec?.enableDHCP || false;
set(this.value.spec, 'enableDHCP', enableDHCP);
set(this.value, 'spec', this.value.spec || { set(this.value, 'spec', this.value.spec || {
cidrBlock: '', cidrBlock: '',
protocol: NETWORK_PROTOCOL.IPv4, protocol: NETWORK_PROTOCOL.IPv4,
@@ -52,9 +46,7 @@ export default {
vpc, vpc,
gatewayIP: '', gatewayIP: '',
excludeIps: [], excludeIps: [],
private: false, private: false
enableDHCP,
acls: []
}); });
}, },
@@ -89,6 +81,7 @@ export default {
protocolOptions() { protocolOptions() {
return Object.values(NETWORK_PROTOCOL); return Object.values(NETWORK_PROTOCOL);
}, },
provider: { provider: {
get() { get() {
const raw = this.value.spec.provider; const raw = this.value.spec.provider;
@@ -132,21 +125,10 @@ export default {
} }
}, },
watch: {
'value.spec.enableDHCP': {
handler(newValue) {
if (newValue === false) {
this.value.spec.dhcpV4Options = '';
this.value.spec.dhcpV6Options = '';
}
},
}
},
methods: { methods: {
async saveSubnet(buttonCb) { async saveSubnet(buttonCb) {
const errors = []; const errors = [];
const name = this.value?.metadata?.name; const name = this.value?.metadata?.name;
const hasEmptyAcls = this.value?.spec?.acls?.some((acl) => !acl.match || !acl.action || acl.priority === undefined || acl.priority === null);
try { try {
if (!name) { if (!name) {
@@ -157,8 +139,6 @@ export default {
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.provider.label') }, true)); errors.push(this.t('validation.required', { key: this.t('harvester.subnet.provider.label') }, true));
} else if (this.value.spec.excludeIps.includes('')) { } else if (this.value.spec.excludeIps.includes('')) {
errors.push(this.t('harvester.validation.subnet.excludeIps')); errors.push(this.t('harvester.validation.subnet.excludeIps'));
} else if (hasEmptyAcls) {
errors.push(this.t('harvester.validation.subnet.aclEmptyError'));
} }
if (errors.length > 0) { if (errors.length > 0) {
@@ -265,45 +245,6 @@ export default {
/> />
</div> </div>
</div> </div>
<div class="row mt-20">
<div class="col span-6">
<RadioGroup
v-model:value="value.spec.enableDHCP"
name="enabled"
:options="[true, false]"
:label="t('harvester.subnet.dhcp.label')"
:labels="[t('generic.enabled'), t('generic.disabled')]"
:mode="mode"
:tooltip="t('harvester.subnet.dhcp.tooltip')"
/>
<LabeledInput
v-if="value.spec.enableDHCP && value.spec.protocol === 'IPv4'"
v-model:value="value.spec.dhcpV4Options"
class="mb-20 mt-20"
:placeholder="t('harvester.subnet.dhcp.placeholder')"
:label="t('harvester.subnet.dhcp.v4Options')"
:mode="mode"
/>
<LabeledInput
v-if="value.spec.enableDHCP && value.spec.protocol === 'IPv6'"
v-model:value="value.spec.dhcpV6Options"
:placeholder="t('harvester.subnet.dhcp.placeholder')"
class="mb-20 mt-20"
:label="t('harvester.subnet.dhcp.v6Options')"
:mode="mode"
/>
<Banner
v-if="value.spec.enableDHCP"
color="info"
class="dhcpOption-banner"
>
<t
k="harvester.subnet.dhcp.dhcpOptionBanner"
:raw="true"
/>
</Banner>
</div>
</div>
<div class="row mt-20"> <div class="row mt-20">
<div class="col span-6"> <div class="col span-6">
<RadioGroup <RadioGroup
@@ -377,23 +318,6 @@ export default {
</template> </template>
</ArrayList> </ArrayList>
</Tab> </Tab>
<Tab
name="ACL"
:label="t('harvester.subnet.acl.label')"
:weight="-2"
class="bordered-table"
>
<AccessControlList
v-model:value="value.spec.acls"
:mode="mode"
/>
</Tab>
</ResourceTabs> </ResourceTabs>
</CruResource> </CruResource>
</template> </template>
<style lang="scss" scoped>
.dhcpOption-banner {
width: max-content;
}
</style>
@@ -2,17 +2,12 @@
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { _EDIT, _VIEW } from '@shell/config/query-params'; import { _EDIT, _VIEW } from '@shell/config/query-params';
import { removeAt } from '@shell/utils/array'; import { removeAt } from '@shell/utils/array';
import { Banner } from '@components/Banner';
import { DOC } from '../../config/doc-links';
import { docLink } from '../../utils/feature-flags';
export default { export default {
name: 'StaticRoutes', name: 'StaticRoutes',
emits: ['update:value'], emits: ['update:value'],
components: { Banner },
props: { props: {
value: { value: {
type: Array, type: Array,
@@ -50,12 +45,6 @@ export default {
nextHopIPTooltip() { nextHopIPTooltip() {
return this.t('harvester.vpc.staticRoutes.nextHopIP.tooltip'); return this.t('harvester.vpc.staticRoutes.nextHopIP.tooltip');
}, },
vpcPeeringExamplesLink() {
const version = this.$store.getters['harvester-common/getServerVersion']();
return docLink(DOC.VPC_CONFIGURATION_EXAMPLES, version);
},
}, },
created() { created() {
@@ -89,13 +78,6 @@ export default {
<template> <template>
<div> <div>
<Banner color="info">
<t
k="harvester.vpc.vpcPeerings.infoBanner"
:raw="true"
:url="vpcPeeringExamplesLink"
/>
</Banner>
<div <div
v-if="rows.length" v-if="rows.length"
class="static-route-row" class="static-route-row"
@@ -2,44 +2,23 @@
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: { components: { UnitInput, InputOrDisplay },
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',
@@ -50,59 +29,21 @@ 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: {
isView() { cupDisplay() {
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;
} }
}, },
@@ -114,60 +55,35 @@ 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: {
hotPlugChanged(neu) { change() {
// If hot plug is enabled, we need to update the maxCpu and maxMemory values let memory = '';
if (neu) {
this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null; if (String(this.localMemory).includes('Gi')) {
this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null; memory = this.localMemory;
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, neu);
} else { } else {
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, '', null, neu); memory = `${ this.localMemory }${ GIBIBYTE }`;
} }
}, if (memory.includes('null')) {
changeMemory() { memory = null;
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); this.$emit('updateCpuMemory', this.localCpu, memory);
},
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>
<div class="row"> <div class="row">
<div class="col span-6 mb-10"> <div class="col span-6 mb-10">
<InputOrDisplay <InputOrDisplay
name="CPU" name="CPU"
:value="cpuDisplay" :value="cupDisplay"
:mode="mode" :mode="mode"
class="mb-10"
> >
<UnitInput <UnitInput
v-model:value="localCpu" v-model:value="localCpu"
@@ -178,10 +94,11 @@ export default {
:disabled="disabled" :disabled="disabled"
:mode="mode" :mode="mode"
class="mb-20" class="mb-20"
@update:value="changeCPU" @update:value="change"
/> />
</InputOrDisplay> </InputOrDisplay>
</div> </div>
<div class="col span-6 mb-10"> <div class="col span-6 mb-10">
<InputOrDisplay <InputOrDisplay
:name="t('harvester.virtualMachine.input.memory')" :name="t('harvester.virtualMachine.input.memory')"
@@ -200,75 +117,9 @@ export default {
required required
:suffix="GIBIBYTE" :suffix="GIBIBYTE"
class="mb-20" class="mb-20"
@update:value="changeMemory" @update:value="change"
/> />
</InputOrDisplay> </InputOrDisplay>
</div> </div>
</div> </div>
<div
v-if="isCPUMemoryHotPlugFeatureEnabled"
class="row"
>
<Checkbox
v-model:value="localEnableHotPlug"
class="check"
type="checkbox"
:label="t('harvester.virtualMachine.hotplug.title')"
: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"
class="mt-20"
>
<UnitInput
v-model:value="maxLocalCpu"
:label="t('harvester.virtualMachine.input.maxCpu')"
suffix="C"
:delay="0"
:disabled="disabled"
:mode="mode"
class="mt-20"
@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>
</template> </template>
@@ -17,7 +17,6 @@ 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'],
@@ -267,17 +266,7 @@ 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>
@@ -358,7 +347,6 @@ 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"
@@ -64,12 +64,7 @@ export default {
isVirtualType: { isVirtualType: {
type: Boolean, type: Boolean,
default: true default: true
}, }
isResizeDisabled: {
type: Boolean,
default: false
},
}, },
data() { data() {
@@ -140,6 +135,10 @@ 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);
}, },
@@ -351,7 +350,7 @@ export default {
:label="t('harvester.fields.size')" :label="t('harvester.fields.size')"
:mode="mode" :mode="mode"
:required="validateRequired" :required="validateRequired"
:disabled="isResizeDisabled" :disable="isLonghornV2"
:suffix="GIBIBYTE" :suffix="GIBIBYTE"
@update:value="update" @update:value="update"
/> />
@@ -13,7 +13,6 @@ 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 {
@@ -56,12 +55,7 @@ export default {
isVirtualType: { isVirtualType: {
type: Boolean, type: Boolean,
default: true default: true
}, }
isResizeDisabled: {
type: Boolean,
default: false
},
}, },
data() { data() {
@@ -124,22 +118,19 @@ export default {
storageClassOptions() { storageClassOptions() {
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => { return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name; const 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) || {};
@@ -311,7 +302,7 @@ export default {
:mode="mode" :mode="mode"
:required="validateRequired" :required="validateRequired"
:label="t('harvester.fields.size')" :label="t('harvester.fields.size')"
:disabled="isResizeDisabled" :disabled="isLonghornV2"
:suffix="GIBIBYTE" :suffix="GIBIBYTE"
@update:value="update" @update:value="update"
/> />
@@ -338,15 +338,6 @@ export default {
saveVM(buttonCb) { saveVM(buttonCb) {
clear(this.errors); clear(this.errors);
this.validateCPUMemory();
// block create VM flow if has validation errors
if (this.errors.length) {
buttonCb(false);
return;
}
if (this.isSingle) { if (this.isSingle) {
this.saveSingle(buttonCb); this.saveSingle(buttonCb);
} else { } else {
@@ -354,18 +345,6 @@ export default {
} }
}, },
validateCPUMemory() {
const { cpu, memory } = this;
if ((!cpu)) {
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.cpu') }, true));
}
if ((!memory)) {
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.memory') }, true));
}
},
async saveSingle(buttonCb) { async saveSingle(buttonCb) {
this.parseVM(); this.parseVM();
this.value.spec.template.spec.hostname = this.hostname ? this.hostname : this.value.metadata.name; this.value.spec.template.spec.hostname = this.hostname ? this.hostname : this.value.metadata.name;
@@ -624,13 +603,9 @@ export default {
:label="t('harvester.virtualMachine.detail.tabs.basics')" :label="t('harvester.virtualMachine.detail.tabs.basics')"
> >
<CpuMemory <CpuMemory
class="mb-20"
:cpu="cpu" :cpu="cpu"
:max-cpu="maxCpu"
:memory="memory" :memory="memory"
:max-memory="maxMemory"
:mode="mode" :mode="mode"
:enable-hot-plug="cpuMemoryHotplugEnabled"
@updateCpuMemory="updateCpuMemory" @updateCpuMemory="updateCpuMemory"
/> />
@@ -892,10 +867,7 @@ export default {
/> />
</div> </div>
<div <div class="col span-6">
v-if="!value.vmMachineTypeAutoFeatureEnabled"
class="col span-6"
>
<LabeledSelect <LabeledSelect
v-model:value="machineType" v-model:value="machineType"
label-key="harvester.virtualMachine.input.MachineType" label-key="harvester.virtualMachine.input.MachineType"
@@ -1,42 +0,0 @@
<script>
import NetworkPolicy from '@shell/edit/networking.k8s.io.networkpolicy';
import { Banner } from '@components/Banner';
import { DOC } from '../config/doc-links';
import { docLink } from '../utils/feature-flags';
export default {
components: {
NetworkPolicy,
Banner
},
computed: {
networkPolicyDocLink() {
const version = this.$store.getters['harvester-common/getServerVersion']();
return docLink(DOC.NETWORK_POLICY, version);
}
}
};
</script>
<template>
<div class="network-policy-edit">
<Banner color="info">
<t
k="harvester.networkPolicy.banner"
:url="networkPolicyDocLink"
:raw="true"
/>
</Banner>
<NetworkPolicy v-bind="$attrs" />
</div>
</template>
<style lang="scss" scoped>
.network-policy-edit {
flex: auto;
display: flex;
flex-direction: column;
}
</style>
+5 -79
View File
@@ -157,10 +157,6 @@ harvester:
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:
@@ -217,7 +213,6 @@ 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
@@ -236,7 +231,6 @@ harvester:
shutdown: Shut Down shutdown: Shut Down
powerOn: Power On powerOn: Power On
reboot: Reboot reboot: Reboot
reconnect: Reconnect
forceStop: Force Stop forceStop: Force Stop
formatters: formatters:
hardwareResourceGauge: hardwareResourceGauge:
@@ -404,7 +398,7 @@ harvester:
sha512: 'Invalid SHA512 checksum.' sha512: 'Invalid SHA512 checksum.'
subnet: subnet:
excludeIps: 'Exclude IPs cannot be empty. Please remove or fill in the exclude IPs.' excludeIps: 'Exclude IPs cannot be empty. Please remove or fill in the exclude IPs.'
aclEmptyError: The fields in subnet access control list rule can not be empty.
dashboard: dashboard:
label: Dashboard label: Dashboard
header: "Harvester Cluster: {cluster}" header: "Harvester Cluster: {cluster}"
@@ -577,10 +571,6 @@ 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:
@@ -726,9 +716,6 @@ 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
@@ -1045,13 +1032,6 @@ harvester:
gateway: gateway:
label: Gateway IP label: Gateway IP
placeholder: e.g. 172.20.0.1 placeholder: e.g. 172.20.0.1
dhcp:
label: Dynamic Host Configuration Protocol (DHCP)
v4Options: DHCPV4Options
v6Options: DHCPV6Options
placeholder: key1=value1, key2=value2
dhcpOptionBanner: DHCP options is a key/value string concatenate with comma. For more detail, please refer to <a href="https://kubeovn.github.io/docs/v1.13.x/en/kubevirt/dhcp/" target="_blank">KubOVN document</a>
tooltip: Enable DHCP server for this subnet. When enabled, VMs can automatically obtain IP addresses from this subnet.
private: private:
label: Private Subnet label: Private Subnet
tooltip: Enable network isolation for this Subnet. When enabled, VMs can only communicate within this subnet, even if other subnets exist under the same VPC. tooltip: Enable network isolation for this Subnet. When enabled, VMs can only communicate within this subnet, even if other subnets exist under the same VPC.
@@ -1062,23 +1042,6 @@ harvester:
placeholder: e.g. 172.16.0.0/16 placeholder: e.g. 172.16.0.0/16
excludeIPs: excludeIPs:
tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list. tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list.
acl:
label: Access Control List
tooltip: The ACL to apply to this Subnet. Must be one of the ACLs in the same namespace.
action:
label: Action
placeholder: Please select an action
direction:
label: Direction
placeholder: Please select a direction
addRule: Add Rule
priority:
label: Priority
placeholder: Please select a priority
match:
label: Match
placeholder: e.g. ip4.dst == 10.10.0.2
banner: The supported field in ACL match can refer to <a href="https://kubeovn.github.io/docs/v1.14.x/en/guide/subnet/#subnet-acl" target="_blank">KubeOvn Subnet ACL document</a>
vpc: vpc:
noAddonEnabled: noAddonEnabled:
@@ -1105,10 +1068,8 @@ harvester:
placeholder: e.g. 169.254.0.1/16 placeholder: e.g. 169.254.0.1/16
remoteVpc: remoteVpc:
label: Remote VPC label: Remote VPC
infoBanner: The static route destination CIDR must cover all subnets CIDR from remote VPC Peer. Read <a href="{url}" target="_blank">VPC Peering Configuration Examples</a> for more information.
networkPolicy:
label: Network Policies
banner: The network policies must be used for VMs attached to overlay networks. Please read the <a href="{url}" target="_blank">harvester document</a> how the network policy works.
network: network:
label: Virtual Machine Networks label: Virtual Machine Networks
tabs: tabs:
@@ -1165,9 +1126,6 @@ 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
@@ -1411,32 +1369,6 @@ harvester:
allowedTopologies: allowedTopologies:
title: Allowed Topologies title: Allowed Topologies
tooltip: Allowed Topologies helps scheduling virtual machines on hosts which match all of below expressions. tooltip: Allowed Topologies helps scheduling virtual machines on hosts which match all of below expressions.
internal:
label: Internal Storage Class
cannotDeleteTooltip: Internal storage class volumes cannot be deleted
cannotDeleteOrDefaultTooltip: Internal storage classes cannot be deleted or set as default
cdiSettings:
label: CDI Settings
volumeModeAccessModes:
label: Volume Mode / Access Modes
tooltip: Specifies the default volume mode and access modes.
add: Add
volumeMode: Volume Mode
accessModes:
label: Access Modes
volumeSnapshotClass:
label: Volume Snapshot Class
tooltip: Sets the Volume Snapshot Class name to be used when taking snapshots of PVCs under this StorageClass.
cloneStrategy:
label: Clone Strategy
tooltip: Defines the clone strategy to use for volumes created with this StorageClass.
copy: Copy
snapshot: Snapshot
csiClone: CSI Clone
fileSystemOverhead:
label: File System Overhead
tooltip: Specifies the percentage of filesystem overhead to consider when calculating PVC size.
placeholder: e.g. 0.05 (up to 3 decimal places, between 0 and 1)
vlanConfig: vlanConfig:
title: Network Configuration title: Network Configuration
@@ -1747,8 +1679,9 @@ advancedSettings:
'harv-server-version': Harvester server version. 'harv-server-version': Harvester server version.
'harv-upgrade-checker-enabled': Specifies whether to enable Harvester upgrade check or not. Default is True. 'harv-upgrade-checker-enabled': Specifies whether to enable Harvester upgrade check or not. Default is True.
'harv-upgrade-checker-url': Default Harvester upgrade check url. Only used when the <code>upgrade-checker-enabled</code> is equal to True. 'harv-upgrade-checker-url': Default Harvester upgrade check url. Only used when the <code>upgrade-checker-enabled</code> is equal to True.
'harv-ui-source': Configure how to load the UI source. The `External` option is used to load the UI from a ui-index URL, while the `Bundled` option loads the UI from the local Harvester server. 'harv-ui-source': Configure how to load the UI source.
'harv-ui-index': 'HTML index location for the UI.' 'harv-ui-index': 'HTML index location for the UI.'
'harv-ui-plugin-index': 'JS index location for the Harvester plugin UI.'
'harv-cluster-registration-url': Registration URL for multi-cluster management. 'harv-cluster-registration-url': Registration URL for multi-cluster management.
'harv-http-proxy': 'HTTP proxy for Harvester to access external services.' 'harv-http-proxy': 'HTTP proxy for Harvester to access external services.'
'harv-additional-ca': 'Custom CA root certificates for TLS validation.' 'harv-additional-ca': 'Custom CA root certificates for TLS validation.'
@@ -1771,8 +1704,6 @@ advancedSettings:
'harv-additional-guest-memory-overhead-ratio': 'The ratio for kubevirt to adjust the VM overhead memory. The value could be zero, empty value or floating number between 1.0 and 10.0, default to 1.5.' 'harv-additional-guest-memory-overhead-ratio': 'The ratio for kubevirt to adjust the VM overhead memory. The value could be zero, empty value or floating number between 1.0 and 10.0, default to 1.5.'
'harv-upgrade-config': 'Configure image preloading and VM restore options for upgrades. See related fields in <a href="{url}" target="_blank" rel="noopener">settings/upgrade-config</a>' 'harv-upgrade-config': 'Configure image preloading and VM restore options for upgrades. See related fields in <a href="{url}" target="_blank" rel="noopener">settings/upgrade-config</a>'
'harv-vm-migration-network': 'Segregated network for VM migration traffic.' 'harv-vm-migration-network': 'Segregated network for VM migration traffic.'
'harv-rancher-cluster': 'Configure Rancher cluster integration settings for guest cluster management.'
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
typeLabel: typeLabel:
kubevirt.io.virtualmachine: |- kubevirt.io.virtualmachine: |-
@@ -1835,11 +1766,6 @@ typeLabel:
one { Virtual Private Cloud } one { Virtual Private Cloud }
other { Virtual Private Clouds } other { Virtual Private Clouds }
} }
networking.k8s.io.networkpolicy: |-
{count, plural,
one { Network Policy }
other { Network Policies }
}
harvesterhci.io.cloudtemplate: |- harvesterhci.io.cloudtemplate: |-
{count, plural, {count, plural,
one { Cloud Configuration Template } one { Cloud Configuration Template }
@@ -83,34 +83,6 @@ export default {
:rows="rows" :rows="rows"
:schema="schema" :schema="schema"
:headers="headers" :headers="headers"
>
<template #cell:name="{ row }">
<td>
<div>
<router-link
v-if="row?.detailLocation"
:to="row.detailLocation"
>
{{ row.nameDisplay }}
<i
v-if="row.isInternalStorageClass && typeof row.isInternalStorageClass === 'function' ? row.isInternalStorageClass() : false"
v-clean-tooltip="t('harvester.storage.internal.cannotDeleteOrDefaultTooltip')"
class="icon icon-info text-info"
style="margin-left: 0.4em;"
/> />
</router-link>
<span v-else>
{{ row.nameDisplay }}
<i
v-if="row.isInternalStorageClass && typeof row.isInternalStorageClass === 'function' ? row.isInternalStorageClass() : false"
v-clean-tooltip="t('harvester.storage.internal.cannotDeleteOrDefaultTooltip')"
class="icon icon-info text-info"
style="margin-left: 0.4em;"
/>
</span>
</div>
</td>
</template>
</ResourceTable>
</div> </div>
</template> </template>
@@ -4,7 +4,7 @@ import Loading from '@shell/components/Loading';
import MessageLink from '@shell/components/MessageLink'; import MessageLink from '@shell/components/MessageLink';
import Masthead from '@shell/components/ResourceList/Masthead'; import Masthead from '@shell/components/ResourceList/Masthead';
import ResourceTable from '@shell/components/ResourceTable'; import ResourceTable from '@shell/components/ResourceTable';
import { isBackupTargetSettingEmpty } from '../utils/setting';
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers'; import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
import FilterVMSchedule from '../components/FilterVMSchedule'; import FilterVMSchedule from '../components/FilterVMSchedule';
import { HCI } from '../types'; import { HCI } from '../types';
@@ -39,7 +39,7 @@ export default {
this.settings = hash.settings; this.settings = hash.settings;
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.SETTING)) { if (this.$store.getters[`${ inStore }/schemaFor`](HCI.SETTING)) {
const backupTargetResource = hash.settings.find( (O) => O.id === 'backup-target'); const backupTargetResource = hash.settings.find( (O) => O.id === 'backup-target');
const isEmpty = isBackupTargetSettingEmpty(backupTargetResource); const isEmpty = this.getBackupTargetValueIsEmpty(backupTargetResource);
if (backupTargetResource && !isEmpty) { if (backupTargetResource && !isEmpty) {
this.testConnect(); this.testConnect();
@@ -78,6 +78,20 @@ export default {
} }
}, },
getBackupTargetValueIsEmpty(resource) {
let out = true;
if (resource?.value) {
try {
const valueJson = JSON.parse(resource?.value);
out = !valueJson.type;
} catch (e) {}
}
return out;
},
getRow(row) { getRow(row) {
return row.status && row.status.source; return row.status && row.status.source;
}, },
@@ -169,9 +183,11 @@ export default {
backupTargetResource() { backupTargetResource() {
return this.settings.find((O) => O.id === 'backup-target'); return this.settings.find((O) => O.id === 'backup-target');
}, },
isEmptyValue() { isEmptyValue() {
return isBackupTargetSettingEmpty(this.backupTargetResource); return this.getBackupTargetValueIsEmpty(this.backupTargetResource);
}, },
canUpdate() { canUpdate() {
return this?.backupTargetResource?.canUpdate; return this?.backupTargetResource?.canUpdate;
}, },
@@ -3,8 +3,6 @@ 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',
@@ -55,13 +53,6 @@ export default {
return base; return base;
}, },
isInternalStorageClass(row) {
const name = row?.spec?.targetStorageClassName ||
row?.metadata?.annotations?.[HCI_ANNOTATIONS.STORAGE_CLASS];
return isInternalStorageClass(name);
},
} }
}; };
</script> </script>
@@ -8,7 +8,6 @@ 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,
@@ -131,10 +130,6 @@ 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);
} }
}, },
@@ -22,8 +22,8 @@ export const VM_HEADERS = [
{ {
name: 'CPU', name: 'CPU',
label: 'CPU', label: 'CPU',
sort: ['displayCPU'], sort: ['spec.template.spec.domain.cpu.cores'],
value: 'displayCPU', value: 'spec.template.spec.domain.cpu.cores',
align: 'center', align: 'center',
dashIfEmpty: true, dashIfEmpty: true,
}, },
@@ -74,7 +74,7 @@ export default {
async fetch() { async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
const _hash = { const _hash = {
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM, opt: { force: true } }), vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
pod: this.$store.dispatch(`${ inStore }/findAll`, { type: POD }), pod: this.$store.dispatch(`${ inStore }/findAll`, { type: POD }),
pvcs: this.$store.dispatch(`${ inStore }/findAll`, { type: PVC }), pvcs: this.$store.dispatch(`${ inStore }/findAll`, { type: PVC }),
pvs: this.$store.dispatch(`${ inStore }/findAll`, { type: PV }), pvs: this.$store.dispatch(`${ inStore }/findAll`, { type: PV }),
+17 -118
View File
@@ -3,9 +3,10 @@ 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';
@@ -163,9 +164,6 @@ 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,
@@ -273,7 +271,7 @@ export default {
needNewSecret() { needNewSecret() {
// When creating a template it is always necessary to create a new secret. // When creating a template it is always necessary to create a new secret.
return this.isCreate ? true : this.showYaml ? false : this.resourceType === HCI.VM_VERSION; return this.showYaml ? false : this.resourceType === HCI.VM_VERSION || this.isCreate;
}, },
defaultTerminationSetting() { defaultTerminationSetting() {
@@ -299,11 +297,9 @@ export default {
async created() { async created() {
await this.$store.dispatch(`${ this.inStore }/findAll`, { type: SECRET }); await this.$store.dispatch(`${ this.inStore }/findAll`, { type: SECRET });
if (!this.value.vmMachineTypeAutoFeatureEnabled) { if (this.value.vmMachineTypesFeatureEnabled) {
try { try {
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']( const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/clusters/local?link=machineTypes');
'v1/harvester/clusters/local?link=machineTypes'
);
const machineTypes = await this.$store.dispatch('harvester/request', { url }); const machineTypes = await this.$store.dispatch('harvester/request', { url });
this.machineTypes = machineTypes; this.machineTypes = machineTypes;
@@ -352,11 +348,8 @@ 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 { const cpu = spec.template.spec.domain?.cpu?.cores;
cpu, memory, maxCpu, maxMemory, isHotplugEnabled const memory = spec.template.spec.domain.resources.limits.memory;
} = 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;
@@ -416,9 +409,6 @@ 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;
@@ -643,26 +633,20 @@ export default {
this.spec.template.spec.domain.machine['type'] = this.machineType; this.spec.template.spec.domain.machine['type'] = this.machineType;
} }
this.setCPUAndMemory(); this.spec.template.spec.domain.cpu.cores = this.cpu;
// update terminationGracePeriodSeconds this.spec.template.spec.domain.resources.limits.cpu = this.cpu ? this.cpu.toString() : this.cpu;
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 {
@@ -670,41 +654,10 @@ 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.maxSockets = 1;
this.spec.template.spec.domain.cpu.sockets = 1;
this.spec.template.spec.domain.cpu.cores = this.cpu;
this.spec.template.spec.domain.resources.limits.cpu = this.cpu?.toString();
this.spec.template.spec.domain.resources.limits.memory = this.memory;
// clean below fields as we don't need them if not enable CPU and memory hotplug
if (this.spec?.template?.spec?.domain?.memory?.maxGuest) {
delete this.spec.template.spec.domain.memory.maxGuest;
}
}
},
parseDiskRows(disk) { parseDiskRows(disk) {
const disks = []; const disks = [];
const volumes = []; const volumes = [];
const diskNameLabels = []; const diskNameLables = [];
const volumeClaimTemplates = []; const volumeClaimTemplates = [];
disk.forEach( (R, index) => { disk.forEach( (R, index) => {
@@ -726,7 +679,7 @@ export default {
disks.push(_disk); disks.push(_disk);
volumes.push(_volume); volumes.push(_volume);
diskNameLabels.push(dataVolumeName); diskNameLables.push(dataVolumeName);
if (R.source !== SOURCE_TYPE.CONTAINER) { if (R.source !== SOURCE_TYPE.CONTAINER) {
volumeClaimTemplates.push(_dataVolumeTemplate); volumeClaimTemplates.push(_dataVolumeTemplate);
@@ -767,9 +720,6 @@ export default {
} }
} }
const specDisks = this.spec?.template?.spec?.domain?.devices?.disks;
const mergedDisks = this.mergeDeviceList(specDisks, disks);
let spec = { let spec = {
...this.spec, ...this.spec,
runStrategy: this.runStrategy, runStrategy: this.runStrategy,
@@ -792,7 +742,7 @@ export default {
...this.spec.template?.spec?.domain, ...this.spec.template?.spec?.domain,
devices: { devices: {
...this.spec.template?.spec?.domain?.devices, ...this.spec.template?.spec?.domain?.devices,
disks: mergedDisks, disks,
}, },
}, },
volumes, volumes,
@@ -891,16 +841,13 @@ export default {
interfaces.push(_interface); interfaces.push(_interface);
}); });
const specInterfaces = this.spec?.template?.spec?.domain?.devices?.interfaces;
const mergedInterfaces = this.mergeInterfaceList(specInterfaces, interfaces);
const spec = { const spec = {
...this.spec.template.spec, ...this.spec.template.spec,
domain: { domain: {
...this.spec.template.spec.domain, ...this.spec.template.spec.domain,
devices: { devices: {
...this.spec.template.spec.domain.devices, ...this.spec.template.spec.domain.devices,
interfaces: mergedInterfaces, interfaces,
}, },
}, },
networks networks
@@ -1005,12 +952,9 @@ export default {
this['sshKey'] = neu; this['sshKey'] = neu;
}, },
updateCpuMemory(cpu, memory, maxCpu = '', maxMemory = null, cpuMemoryHotplugEnabled = false) { updateCpuMemory(cpu, memory) {
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) {
@@ -1043,9 +987,7 @@ export default {
}, },
parseVolumeClaimTemplate(R, dataVolumeName) { parseVolumeClaimTemplate(R, dataVolumeName) {
const sizeString = String(R.size); if (!String(R.size).includes('Gi') && R.size) {
if (!(sizeString.includes('Gi') || sizeString.includes('Ti')) && R.size) {
R.size = `${ R.size }${ GIBIBYTE }`; R.size = `${ R.size }${ GIBIBYTE }`;
} }
@@ -1536,49 +1478,6 @@ export default {
this.refreshYamlEditor(); this.refreshYamlEditor();
}, },
mergeInterfaceList(specInterfaceList, interfaceList) {
if (!specInterfaceList || specInterfaceList.length === 0) {
return interfaceList;
}
const specInterfaceMap = new Map(specInterfaceList.map((iface) => [iface.name, iface]));
return interfaceList.map((iface) => {
const specInterface = specInterfaceMap.get(iface.name);
if (specInterface) {
const merged = { ...specInterface, ...iface };
// currently we only have bridge and masquerade network type, they are mutually exclusive
if (iface['bridge']) {
delete merged['masquerade'];
} else {
delete merged['bridge'];
}
return merged;
}
return iface;
});
},
mergeDeviceList(specDeviceList, deviceList) {
if (!specDeviceList || specDeviceList.length === 0) {
return deviceList;
}
const specDeviceMap = new Map(specDeviceList.map((device) => [device.name, device]));
return deviceList.map((device) => {
const specDevice = specDeviceMap.get(device.name);
if (specDevice) {
return { ...specDevice, ...device };
}
return device;
});
},
refreshYamlEditor() { refreshYamlEditor() {
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.yamlEditor?.updateValue(); this.$refs.yamlEditor?.updateValue();
+1 -3
View File
@@ -1,19 +1,17 @@
import SteveModel from '@shell/plugins/steve/steve-class'; import SteveModel from '@shell/plugins/steve/steve-class';
import { VIEW_IN_API, DEV } from '@shell/store/prefs'; import { VIEW_IN_API, DEV } from '@shell/store/prefs';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester'; import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
import { HARVESTER_CRD_MAP } from '../config/harvester-map';
export default class HarvesterResource extends SteveModel { export default class HarvesterResource extends SteveModel {
get listLocation() { get listLocation() {
const name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`; const name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
const resource = HARVESTER_CRD_MAP[this.type] || this.type;
return this.$rootGetters['type-map/optionsFor'](this.type).customRoute || { return this.$rootGetters['type-map/optionsFor'](this.type).customRoute || {
name, name,
params: { params: {
product: this.$rootGetters['productId'], product: this.$rootGetters['productId'],
cluster: this.$rootGetters['clusterId'], cluster: this.$rootGetters['clusterId'],
resource resource: this.type,
}, },
}; };
} }
@@ -1,4 +1,3 @@
// This file is Cloud Configuration Templates page model
import { clone } from '@shell/utils/object'; import { clone } from '@shell/utils/object';
import { HCI } from '../../types'; import { HCI } from '../../types';
import HarvesterResource from '../harvester'; import HarvesterResource from '../harvester';
@@ -12,7 +12,7 @@ export default class HarvesterNetworkAttachmentDef extends NetworkAttachmentDef
params: { params: {
product: HARVESTER_PRODUCT, product: HARVESTER_PRODUCT,
cluster: this.$rootGetters['clusterId'], cluster: this.$rootGetters['clusterId'],
resource: HCI.NETWORK_ATTACHMENT, resource: this.type,
}, },
}; };
} }
@@ -9,19 +9,6 @@ export default class HciClusterflow extends HarvesterFlow {
return this.$rootGetters['harvester/all'](LOGGING.CLUSTER_OUTPUT) || []; return this.$rootGetters['harvester/all'](LOGGING.CLUSTER_OUTPUT) || [];
} }
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.CLUSTER_FLOW;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get _detailLocation() { get _detailLocation() {
const schema = this.$getters['schemaFor'](this.type); const schema = this.$getters['schemaFor'](this.type);
@@ -21,19 +21,6 @@ export default class HciClusteroutput extends LogOutput {
}; };
} }
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.CLUSTER_OUTPUT;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get detailLocation() { get detailLocation() {
const detailLocation = clone(this._detailLocation); const detailLocation = clone(this._detailLocation);
@@ -14,19 +14,6 @@ export default class HarvesterLogFlow extends Flow {
return this.$rootGetters['harvester/all'](LOGGING.CLUSTER_OUTPUT) || []; return this.$rootGetters['harvester/all'](LOGGING.CLUSTER_OUTPUT) || [];
} }
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.FLOW;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get _detailLocation() { get _detailLocation() {
const schema = this.$getters['schemaFor'](this.type); const schema = this.$getters['schemaFor'](this.type);
@@ -22,19 +22,6 @@ export default class HarvesterLogOutput extends LogOutput {
}; };
} }
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.OUTPUT;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get detailLocation() { get detailLocation() {
const detailLocation = clone(this._detailLocation); const detailLocation = clone(this._detailLocation);
@@ -19,19 +19,6 @@ export default class HciAlertmanagerConfig extends SteveModel {
}; };
} }
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.ALERTMANAGERCONFIG;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get doneOverride() { get doneOverride() {
const detailLocation = clone(this._detailLocation); const detailLocation = clone(this._detailLocation);
-19
View File
@@ -5,25 +5,6 @@ import Secret from '@shell/models/secret';
import { NAMESPACE } from '@shell/config/types'; import { NAMESPACE } from '@shell/config/types';
export default class HciSecret extends Secret { export default class HciSecret extends Secret {
// prevent harvester secret detail page be overridden.
// See isFullPageOverride in https://github.com/rancher/dashboard/blob/master/shell/components/ResourceDetail/index.vue
get fullDetailPageOverride() {
return false;
}
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.SECRET;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get _detailLocation() { get _detailLocation() {
const schema = this.$getters['schemaFor'](this.type); const schema = this.$getters['schemaFor'](this.type);
@@ -4,24 +4,10 @@ import { HCI } from '../../types';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester'; import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
import { LONGHORN_DRIVER } from '@shell/config/types'; import { LONGHORN_DRIVER } from '@shell/config/types';
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim'; import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
import { isInternalStorageClass } from '../../utils/storage-class';
export const LVM_DRIVER = 'lvm.driver.harvesterhci.io'; export const LVM_DRIVER = 'lvm.driver.harvesterhci.io';
export default class HciStorageClass extends StorageClass { export default class HciStorageClass extends StorageClass {
get listLocation() {
const listLocation = clone(super.listLocation);
listLocation.name = this.harvesterResourcesInExplorer ? 'c-cluster-product-resource' : `${ HARVESTER_PRODUCT }-c-cluster-resource`;
listLocation.params.resource = HCI.STORAGE;
return listLocation;
}
get harvesterResourcesInExplorer() {
return this.$rootGetters['productId'] !== HARVESTER_PRODUCT;
}
get detailLocation() { get detailLocation() {
const detailLocation = clone(this._detailLocation); const detailLocation = clone(this._detailLocation);
@@ -99,23 +85,4 @@ export default class HciStorageClass extends StorageClass {
get thirdPartyStorageFeatureEnabled() { get thirdPartyStorageFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage'); return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
} }
isInternalStorageClass() {
return isInternalStorageClass(this.metadata?.name);
}
get availableActions() {
let out = super.availableActions || [];
if (this.isInternalStorageClass()) {
out = out.filter((action) => {
return !['setDefault', 'setAsDefault', 'promptRemove'].includes(action.action);
});
}
if (out[0] && out[0].divider === true) {
out = out.slice(1);
}
return out;
}
} }
@@ -156,8 +156,4 @@ export default class HciSetting extends HarvesterResource {
return out; return out;
} }
get disableResourceDetailDrawer() {
return true;
}
} }
@@ -25,10 +25,6 @@ export default class HciVmBackup extends HarvesterResource {
return detailLocation; return detailLocation;
} }
get disableResourceDetailDrawerConfigTab() {
return true;
}
get doneOverride() { get doneOverride() {
const route = this.currentRoute(); const route = this.currentRoute();
const detailLocation = clone(this._detailLocation); const detailLocation = clone(this._detailLocation);
@@ -2,7 +2,7 @@ import SteveModel from '@shell/plugins/steve/steve-class';
import { HCI } from '@shell/config/labels-annotations'; import { HCI } from '@shell/config/labels-annotations';
import { NETWORK_TYPE } from '../config/types'; import { NETWORK_TYPE } from '../config/types';
const { UNTAGGED, OVERLAY } = NETWORK_TYPE; const { UNTAGGED } = NETWORK_TYPE;
export default class NetworkAttachmentDef extends SteveModel { export default class NetworkAttachmentDef extends SteveModel {
applyDefaults() { applyDefaults() {
@@ -45,7 +45,7 @@ export default class NetworkAttachmentDef extends SteveModel {
} }
get vlanId() { get vlanId() {
return this.vlanType === UNTAGGED || this.vlanType === OVERLAY ? 'N/A' : this.parseConfig.vlan; return this.vlanType === UNTAGGED ? 'N/A' : this.parseConfig.vlan;
} }
get customValidationRules() { get customValidationRules() {
@@ -68,7 +68,7 @@ export default class NetworkAttachmentDef extends SteveModel {
const route = annotations[HCI.NETWORK_ROUTE]; const route = annotations[HCI.NETWORK_ROUTE];
let config = {}; let config = {};
if (this.vlanType === UNTAGGED || this.vlanType === OVERLAY) { if (this.vlanType === UNTAGGED) {
return 'N/A'; return 'N/A';
} }
@@ -13,8 +13,6 @@ 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';
import { isBackupTargetSettingUnavailable } from '../utils/setting';
export const OFF = 'Off'; export const OFF = 'Off';
@@ -33,6 +31,8 @@ const PAUSED_VM_MODAL_MESSAGE =
const POD_STATUS_NOT_SCHEDULABLE = 'POD_NOT_SCHEDULABLE'; const POD_STATUS_NOT_SCHEDULABLE = 'POD_NOT_SCHEDULABLE';
const POD_STATUS_CONTAINER_FAILING = 'POD_CONTAINER_FAILING'; const POD_STATUS_CONTAINER_FAILING = 'POD_CONTAINER_FAILING';
// eslint-disable-next-line no-unused-vars
const POD_STATUS_NOT_READY = 'POD_NOT_READY';
const POD_STATUS_FAILED = 'POD_FAILED'; const POD_STATUS_FAILED = 'POD_FAILED';
const POD_STATUS_CRASHLOOP_BACKOFF = 'POD_CRASHLOOP_BACKOFF'; const POD_STATUS_CRASHLOOP_BACKOFF = 'POD_CRASHLOOP_BACKOFF';
@@ -153,7 +153,7 @@ export default class VirtVm extends HarvesterResource {
}, },
{ {
action: 'backupVM', action: 'backupVM',
enabled: !!this.actions?.backup && !this.isBackupTargetUnavailable, enabled: !!this.actions?.backup,
icon: 'icon icon-backup', icon: 'icon icon-backup',
label: this.t('harvester.action.backup') label: this.t('harvester.action.backup')
}, },
@@ -169,12 +169,6 @@ 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,
@@ -328,10 +322,6 @@ export default class VirtVm extends HarvesterResource {
}); });
} }
doSoftReboot() {
this.doActionGrowl('softreboot', {});
}
softrebootVM(resources = this) { softrebootVM(resources = this) {
this.$dispatch('promptModal', { this.$dispatch('promptModal', {
resources, resources,
@@ -486,13 +476,6 @@ export default class VirtVm extends HarvesterResource {
}); });
} }
cpuMemoryHotplug(resources = this) {
this.$dispatch('promptModal', {
resources,
component: 'HarvesterCPUMemoryHotPlugDialog'
});
}
abortMigrationVM() { abortMigrationVM() {
this.doActionGrowl('abortMigration', {}); this.doActionGrowl('abortMigration', {});
} }
@@ -745,7 +728,6 @@ export default class VirtVm extends HarvesterResource {
if (this && if (this &&
!this.isVMExpectedRunning && !this.isVMExpectedRunning &&
this.isVMCreated && this.isVMCreated &&
this.vmi?.status?.phase !== undefined &&
this.vmi?.status?.phase !== VMIPhase.Succeeded && this.vmi?.status?.phase !== VMIPhase.Succeeded &&
this.vmi?.status?.phase !== VMIPhase.Pending this.vmi?.status?.phase !== VMIPhase.Pending
) { ) {
@@ -1109,6 +1091,19 @@ 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',
@@ -1132,11 +1127,12 @@ export default class VirtVm extends HarvesterResource {
} }
get memorySort() { get memorySort() {
const memory = getVmCPUMemoryValues(this).memory; const memory =
this?.spec?.template?.spec?.domain?.resources?.requests?.memory || 0;
const formatSize = parseSi(memory); const formatSize = parseSi(memory);
return parseInt(formatSize, 10); return parseInt(formatSize);
} }
get ingoreVMMessage() { get ingoreVMMessage() {
@@ -1162,29 +1158,14 @@ 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');
}
const restoreConditions = this.restoreResource?.status?.conditions || [];
const restoreError = restoreConditions.find((s) => s.error === true && s.reason === 'Error' && s.type === 'Ready');
if (restoreError?.message) {
return restoreError.message;
}
return this.ingoreVMMessage ? '' : super.stateDescription; return this.ingoreVMMessage ? '' : super.stateDescription;
} }
get displayCPU() {
return getVmCPUMemoryValues(this).cpu;
}
get displayMemory() { get displayMemory() {
return getVmCPUMemoryValues(this).memory; return (
this.spec.template.spec.domain.resources?.limits?.memory ||
this.spec.template.spec.domain.resources?.requests?.memory
);
} }
get isQemuInstalled() { get isQemuInstalled() {
@@ -1236,15 +1217,8 @@ export default class VirtVm extends HarvesterResource {
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage'); return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
} }
get vmMachineTypeAutoFeatureEnabled() { get vmMachineTypesFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('vmMachineTypeAuto'); return this.$rootGetters['harvester-common/getFeatureEnabled']('vmMachineTypes');
}
get isBackupTargetUnavailable() {
const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || [];
const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target');
return isBackupTargetSettingUnavailable(backupTargetSetting);
} }
setInstanceLabels(val) { setInstanceLabels(val) {
@@ -16,10 +16,7 @@ export default class HciVlanConfig extends HarvesterResource {
uplink: { uplink: {
nics: [], nics: [],
linkAttributes: {}, linkAttributes: {},
bondOptions: { bondOptions: { mode: 'active-backup' },
mode: 'active-backup',
miimon: -1
},
}, },
}; };
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "harvester", "name": "harvester",
"description": "Rancher UI Extension for Harvester", "description": "Rancher UI Extension for Harvester",
"version": "1.7.0-dev", "version": "1.6.0-rc2",
"private": false, "private": false,
"rancher": { "rancher": {
"annotations": { "annotations": {
-35
View File
@@ -1,35 +0,0 @@
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
export function getVmCPUMemoryValues(vm) {
if (!vm) {
return {
cpu: null,
memory: null,
isHotplugEnabled: false
};
}
const isHotplugEnabled = isCPUMemoryHotPlugEnabled(vm);
const { sockets = 1, threads = 1, cores = null } = vm.spec.template.spec.domain.cpu || {};
const cpu = cores === null ? null : sockets * threads * cores;
if (isHotplugEnabled) {
return {
cpu,
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,
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?.memory?.maxGuest || false;
}
+13 -17
View File
@@ -8,6 +8,7 @@ export const docLink = (suffix, version) => {
}; };
export function getVersion(v) { export function getVersion(v) {
// e.g v1.4.0
if (process.env.VUE_APP_SERVER_VERSION) { if (process.env.VUE_APP_SERVER_VERSION) {
return process.env.VUE_APP_SERVER_VERSION; return process.env.VUE_APP_SERVER_VERSION;
} }
@@ -17,21 +18,20 @@ export function getVersion(v) {
return `v${ semver.major(v) }.${ semver.minor(v) }.${ semver.patch(v) }`; return `v${ semver.major(v) }.${ semver.minor(v) }.${ semver.patch(v) }`;
} catch (error) { } catch (error) {
// fallback to the latest version // fallback to the latest version
return getLatestVersion(); return latestVersion(Object.keys(RELEASE_FEATURES));
} }
} }
function getLatestVersion() { function latestVersion(versions) {
const allVersions = Object.keys(RELEASE_FEATURES).filter(semver.valid).sort(semver.rcompare); return versions.sort((a, b) => semver.compare(a, b)).pop();
return allVersions[0] || '';
} }
function getLatestCompatibleVersion(version) { // v1.3.3 => latest v1.3.x, v1.4.2 => latest v1.4.x
const allVersions = Object.keys(RELEASE_FEATURES).filter(semver.valid); function latestMinorVersion(v) {
const compatible = allVersions.filter((v) => semver.lte(v, version)).sort(semver.rcompare); const minor = `v${ semver.major(v) }.${ semver.minor(v) }`;
const minorVersions = Object.keys(RELEASE_FEATURES).filter((version) => version.startsWith(minor));
return compatible[0] || getLatestVersion(); return latestVersion(minorVersions);
} }
export const featureEnabled = (featureKey, serverVersion) => { export const featureEnabled = (featureKey, serverVersion) => {
@@ -46,16 +46,12 @@ export const featureEnabled = (featureKey, serverVersion) => {
return false; return false;
} }
const compatibleVersion = getLatestCompatibleVersion(version); let releasedFeatures = RELEASE_FEATURES[version];
const releasedFeatures = RELEASE_FEATURES?.[compatibleVersion];
if (!Array.isArray(releasedFeatures)) { if (!releasedFeatures) {
// eslint-disable-next-line no-console const fallback = latestMinorVersion(version);
console.error(
`Feature flags for version ${ version } are not defined. Please upgrade Harvester UI extension and check the support matrix.`
);
return false; releasedFeatures = RELEASE_FEATURES[fallback];
} }
return releasedFeatures.includes(featureKey); return releasedFeatures.includes(featureKey);
-25
View File
@@ -1,25 +0,0 @@
export function isBackupTargetSettingEmpty(setting = {}) {
let isEmpty = true;
if (setting?.value) {
try {
const valueJson = JSON.parse(setting?.value);
isEmpty = !valueJson.type;
} catch (e) {
// eslint-disable-next-line no-console
console.error('Failed to parse backup target value:', e);
}
}
return isEmpty;
}
export function isBackupTargetSettingUnavailable(setting = {}) {
const errorMessage = setting.errorMessage;
const isEmptyValue = isBackupTargetSettingEmpty(setting);
const canUpdate = setting.canUpdate;
return (errorMessage || isEmptyValue) && canUpdate;
}
-6
View File
@@ -1,6 +0,0 @@
import { INTERNAL_STORAGE_CLASS } from '../config/types';
export function isInternalStorageClass(name) {
return name === INTERNAL_STORAGE_CLASS.VMSTATE_PERSISTENCE ||
name === INTERNAL_STORAGE_CLASS.LONGHORN_STATIC;
}
+1 -1
View File
@@ -15,7 +15,7 @@ if [[ "$VERSION" != "$EXTENSION_VERSION" ]]; then
exit 1 exit 1
fi fi
if [[ "${TAG_VERSION}" != "${PKG_VERSION}"* ]]; then if [[ "${TAG_VERSION}" != "$PKG_VERSION" ]]; then
echo "Package version $PKG_VERSION in pkg/harvester/package.json does not match tag version ${TAG_VERSION}" echo "Package version $PKG_VERSION in pkg/harvester/package.json does not match tag version ${TAG_VERSION}"
exit 1 exit 1
fi fi
-16
View File
@@ -1,16 +0,0 @@
/* eslint-disable no-console */
import load from "@commitlint/load";
if (process.argv.length < 3) {
console.error('No PR title as first argument');
process.exit(1);
}
const prTitle = process.argv[2];
const prLabel = prTitle.split(':')[0].trim();
const config = await load({ extends: ["./commitlint.config.js"] });
const commitPrefix = config?.rules?.['type-enum']?.[2] || [];
const label = commitPrefix.includes(prLabel) ? prLabel : 'other';
console.log(label);
+1815 -938
View File
File diff suppressed because it is too large Load Diff