mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-07-03 15:22:22 +00:00
Compare commits
101 Commits
v1.8.1-dev
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5090d8ecad | ||
|
|
b9334bafb7 | ||
|
|
9a675da756 | ||
|
|
8c0c36e022 | ||
|
|
140a85f6a8 | ||
|
|
e62ae7e1d8 | ||
|
|
d03cff645b | ||
|
|
34dfe4027e | ||
|
|
135d520b8d | ||
|
|
62a19ee3fb | ||
|
|
64f0f5fb87 | ||
|
|
93a692ff0c | ||
|
|
5e3f12de35 | ||
|
|
f115261889 | ||
|
|
5985913f5e | ||
|
|
661ab995f6 | ||
|
|
d3f63df883 | ||
|
|
44ef9195eb | ||
|
|
b18aebbbd4 | ||
|
|
d2db23d69a | ||
|
|
55918232a6 | ||
|
|
ce2adbdc3b | ||
|
|
ae65037083 | ||
|
|
b4ea7c8f98 | ||
|
|
836b04f222 | ||
|
|
0908c7fc6b | ||
|
|
b34d618c7c | ||
|
|
8945b9f158 | ||
|
|
c5af2b576b | ||
|
|
45872cef3b | ||
|
|
3e5ee422ce | ||
|
|
75202a9e55 | ||
|
|
5f5ce291fb | ||
|
|
a27e38fc81 | ||
|
|
89e1484884 | ||
|
|
eacca055c7 | ||
|
|
56c1738055 | ||
|
|
8f4b335cae | ||
|
|
91232beffc | ||
|
|
09e8946cc3 | ||
|
|
e70c684382 | ||
|
|
1446aac168 | ||
|
|
8e65274b0c | ||
|
|
e5432210b9 | ||
|
|
af52df0ba0 | ||
|
|
e5a1929ac5 | ||
|
|
9de065a5c9 | ||
|
|
cd933bdbf8 | ||
|
|
5fe642a42d | ||
|
|
18c66083ab | ||
|
|
d291a35754 | ||
|
|
5cc8b4c301 | ||
|
|
032700293c | ||
|
|
8cb793e7ad | ||
|
|
2c45b71d1f | ||
|
|
5a301dcf55 | ||
|
|
1a92265d03 | ||
|
|
8f65915bad | ||
|
|
67bb6dfbd5 | ||
|
|
e74428d951 | ||
|
|
629f7df6b9 | ||
|
|
b7119d5c4c | ||
|
|
6fdd1e3954 | ||
|
|
7d0f33f31d | ||
|
|
9ce95daf76 | ||
|
|
ce72232bc3 | ||
|
|
afc0e0f531 | ||
|
|
e941cc9a90 | ||
|
|
9961523d08 | ||
|
|
c4d1018388 | ||
|
|
be64329776 | ||
|
|
35411ed87a | ||
|
|
15eb0f07f7 | ||
|
|
fb78f24fdd | ||
|
|
81ad827829 | ||
|
|
6dd9b33336 | ||
|
|
1f9e9b336b | ||
|
|
c5b4f6cd1e | ||
|
|
4ce35ce075 | ||
|
|
27c26bd782 | ||
|
|
9d698b1230 | ||
|
|
566e79eda5 | ||
|
|
42ddcfc1fe | ||
|
|
ad3decf71f | ||
|
|
8083a41df0 | ||
|
|
46b860260a | ||
|
|
62801b3b13 | ||
|
|
161e3bbd97 | ||
|
|
97e93dba0b | ||
|
|
9a8a709e56 | ||
|
|
d1949641a7 | ||
|
|
9c9f59c939 | ||
|
|
ccc14c7fb9 | ||
|
|
2ba471907e | ||
|
|
5aea476f64 | ||
|
|
519c7d9f1f | ||
|
|
a9c392c13f | ||
|
|
888ec7a50f | ||
|
|
a2486a7d38 | ||
|
|
df3d249923 | ||
|
|
23344e0c07 |
16
.github/renovate.json
vendored
16
.github/renovate.json
vendored
@ -14,10 +14,17 @@
|
|||||||
"semanticCommits": "enabled",
|
"semanticCommits": "enabled",
|
||||||
"semanticCommitType": "deps",
|
"semanticCommitType": "deps",
|
||||||
"prHourlyLimit": 12,
|
"prHourlyLimit": 12,
|
||||||
|
"digest": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
"timezone": "Asia/Taipei",
|
"timezone": "Asia/Taipei",
|
||||||
"schedule": ["after 10am on sunday"],
|
"schedule": ["after 10am on sunday"],
|
||||||
"postUpdateOptions": ["yarnDedupeFewer"],
|
"postUpdateOptions": ["yarnDedupeFewer"],
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchUpdateTypes": ["digest"],
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"matchUpdateTypes": ["major"],
|
"matchUpdateTypes": ["major"],
|
||||||
"enabled": false
|
"enabled": false
|
||||||
@ -39,11 +46,12 @@
|
|||||||
"reviewers": ["a110605", "houhoucoop"]
|
"reviewers": ["a110605", "houhoucoop"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchUpdateTypes": ["patch", "digest"],
|
"matchUpdateTypes": ["patch"],
|
||||||
"automerge": true,
|
"automerge": false,
|
||||||
"minimumReleaseAge": "7 days",
|
"minimumReleaseAge": "7 days",
|
||||||
"groupName": "patch digest dependencies",
|
"groupName": "patch dependencies",
|
||||||
"labels": ["patch-update", "automerge"]
|
"labels": ["patch-update"],
|
||||||
|
"reviewers": ["a110605", "houhoucoop"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
20
README.md
20
README.md
@ -5,6 +5,19 @@ The Harvester UI Extension is a Rancher extension that provides the user interfa
|
|||||||
> **Note:**
|
> **Note:**
|
||||||
> This extension is available starting from **Rancher 2.10.0**. Ensure your Rancher version is **2.10.0 or later** to access Harvester integration.
|
> This extension is available starting from **Rancher 2.10.0**. Ensure your Rancher version is **2.10.0 or later** to access Harvester integration.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Development Setup](#development-setup)
|
||||||
|
- [Commit Message Guidelines](#commit-message-guidelines)
|
||||||
|
- [Branch Structure](#branch-structure)
|
||||||
|
- [Testing Guidelines](#testing-guidelines)
|
||||||
|
- [Release](#release)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
|
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
|
||||||
@ -157,6 +170,13 @@ To test the standalone UI, configure Harvester to load the UI from an external s
|
|||||||
2. Set **ui-source** to `External`
|
2. Set **ui-source** to `External`
|
||||||
3. Set **ui-index** to the desired URL
|
3. Set **ui-index** to the desired URL
|
||||||
|
|
||||||
|
|
||||||
|
## Release
|
||||||
|
|
||||||
|
The Harvester UI Extension follows the [Harvester](https://github.com/harvester/harvester) release cycle. After RC1 is cut for a new Harvester version, we usually create and work from the corresponding release branch (for example, `release-harvester-v1.8`). The remaining RC builds and the final official release are published from that branch.
|
||||||
|
|
||||||
|
After Harvester releases a new version, update the Harvester entry in rancher/ui-plugin-charts [manifest.json](https://github.com/rancher/ui-plugin-charts/blob/aafd215debbc6cb3100e7ba4b0a542c932397acd/manifest.json#L133-L151). This ensures air-gapped users can pull the new Harvester UI Extension image.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
If you want to contribute, start by reading this document, then visit our [Getting Started guide](https://extensions.rancher.io/extensions/next/extensions-getting-started) to learn how to develop and submit changes.
|
If you want to contribute, start by reading this document, then visit our [Getting Started guide](https://extensions.rancher.io/extensions/next/extensions-getting-started) to learn how to develop and submit changes.
|
||||||
|
|||||||
@ -33,7 +33,7 @@ module.exports = {
|
|||||||
'subject-full-stop': [2, 'never', '.'],
|
'subject-full-stop': [2, 'never', '.'],
|
||||||
'subject-max-length': [0, 'never'],
|
'subject-max-length': [0, 'never'],
|
||||||
'body-leading-blank': [2, 'always'],
|
'body-leading-blank': [2, 'always'],
|
||||||
'body-max-line-length': [2, 'always', 100],
|
'body-max-line-length': [0, 'always', 100],
|
||||||
'footer-leading-blank': [2, 'always'],
|
'footer-leading-blank': [2, 'always'],
|
||||||
'footer-max-line-length': [2, 'always', 100],
|
'footer-max-line-length': [2, 'always', 100],
|
||||||
},
|
},
|
||||||
|
|||||||
14
package.json
14
package.json
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.8.1-dev",
|
"version": "1.9.0-dev",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.0.0"
|
"node": ">=24.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-class-static-block": "7.28.6",
|
"@babel/plugin-transform-class-static-block": "7.29.7",
|
||||||
"@rancher/shell": "3.0.12-rc.1",
|
"@rancher/shell": "3.0.12-rc.3",
|
||||||
"@vue-flow/background": "^1.3.0",
|
"@vue-flow/background": "^1.3.0",
|
||||||
"@vue-flow/controls": "^1.1.1",
|
"@vue-flow/controls": "^1.1.1",
|
||||||
"@vue-flow/core": "^1.33.5",
|
"@vue-flow/core": "^1.33.5",
|
||||||
@ -21,7 +21,7 @@
|
|||||||
"yaml": "^2.5.1"
|
"yaml": "^2.5.1"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/node": "25.6.2",
|
"@types/node": "25.9.4",
|
||||||
"cronstrue": "2.59.0",
|
"cronstrue": "2.59.0",
|
||||||
"d3-color": "3.1.0",
|
"d3-color": "3.1.0",
|
||||||
"ejs": "3.1.10",
|
"ejs": "3.1.10",
|
||||||
@ -33,9 +33,9 @@
|
|||||||
"merge": "2.1.1",
|
"merge": "2.1.1",
|
||||||
"node-forge": "1.4.0",
|
"node-forge": "1.4.0",
|
||||||
"nth-check": "2.1.1",
|
"nth-check": "2.1.1",
|
||||||
"qs": "6.15.1",
|
"qs": "6.15.2",
|
||||||
"roarr": "7.21.4",
|
"roarr": "7.21.6",
|
||||||
"semver": "7.8.0",
|
"semver": "7.8.5",
|
||||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
186
pkg/harvester/components/HarvesterNodeSelector.vue
Normal file
186
pkg/harvester/components/HarvesterNodeSelector.vue
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
<script>
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import MatchExpressions from '@shell/components/form/MatchExpressions';
|
||||||
|
import ResourceTable from '@shell/components/ResourceTable';
|
||||||
|
import { _EDIT } from '@shell/config/query-params';
|
||||||
|
import { convert, simplify, matching as selectorMatching } from '@shell/utils/selector';
|
||||||
|
import throttle from 'lodash/throttle';
|
||||||
|
import { NODE } from '@shell/config/types';
|
||||||
|
import { NAME, AGE } from '@shell/config/table-headers';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterNodeSelector',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Banner,
|
||||||
|
MatchExpressions,
|
||||||
|
ResourceTable,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
this.updateMatchingResources();
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return {
|
||||||
|
matchingResources: {
|
||||||
|
matched: 0,
|
||||||
|
matches: [],
|
||||||
|
none: true,
|
||||||
|
sample: null,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
tableHeaders: [
|
||||||
|
NAME,
|
||||||
|
{
|
||||||
|
name: 'host-ip',
|
||||||
|
labelKey: 'tableHeaders.hostIp',
|
||||||
|
search: ['internalIp'],
|
||||||
|
value: 'internalIp',
|
||||||
|
sort: ['internalIp'],
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cpuManager',
|
||||||
|
labelKey: 'harvester.tableHeaders.cpuManager',
|
||||||
|
value: 'id',
|
||||||
|
formatter: 'HarvesterCPUPinning',
|
||||||
|
width: 150,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'diskState',
|
||||||
|
labelKey: 'tableHeaders.diskState',
|
||||||
|
value: 'diskState',
|
||||||
|
formatter: 'HarvesterDiskState',
|
||||||
|
width: 130,
|
||||||
|
},
|
||||||
|
AGE,
|
||||||
|
],
|
||||||
|
inStore,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler: 'updateMatchingResources',
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
allResourcesInScope() {
|
||||||
|
this.updateMatchingResources();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
schema() {
|
||||||
|
return this.$store.getters[`${ this.inStore }/schemaFor`](NODE);
|
||||||
|
},
|
||||||
|
|
||||||
|
selectorExpressions: {
|
||||||
|
get() {
|
||||||
|
return convert(
|
||||||
|
this.value.matchLabels || {},
|
||||||
|
this.value.matchExpressions || []
|
||||||
|
);
|
||||||
|
},
|
||||||
|
set(selectorExpressions) {
|
||||||
|
const { matchLabels, matchExpressions } = simplify(selectorExpressions);
|
||||||
|
|
||||||
|
this.value['matchLabels'] = matchLabels;
|
||||||
|
this.value['matchExpressions'] = matchExpressions;
|
||||||
|
this.updateMatchingResources();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
allNodes() {
|
||||||
|
return this.$store.getters[`${ this.inStore }/all`](NODE) || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
allResourcesInScope() {
|
||||||
|
return this.allNodes.length;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updateMatchingResources: throttle(function() {
|
||||||
|
const expressions = this.selectorExpressions;
|
||||||
|
const allNodes = this.allNodes;
|
||||||
|
|
||||||
|
// Empty expressions with no key = no match
|
||||||
|
const hasValidExpression = expressions.length > 0 && expressions.every((e) => !!e.key);
|
||||||
|
|
||||||
|
if (!hasValidExpression) {
|
||||||
|
this.matchingResources = {
|
||||||
|
matched: 0,
|
||||||
|
matches: [],
|
||||||
|
none: true,
|
||||||
|
sample: null,
|
||||||
|
total: allNodes.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = selectorMatching(allNodes, expressions, 'metadata.labels');
|
||||||
|
|
||||||
|
this.matchingResources = {
|
||||||
|
matched: matches.length,
|
||||||
|
matches,
|
||||||
|
none: matches.length === 0,
|
||||||
|
sample: matches[0]?.nameDisplay || null,
|
||||||
|
total: allNodes.length,
|
||||||
|
};
|
||||||
|
}, 100, { trailing: true })
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<MatchExpressions
|
||||||
|
v-model:value="selectorExpressions"
|
||||||
|
:mode="mode"
|
||||||
|
:show-remove="false"
|
||||||
|
:type="'node'"
|
||||||
|
:target-resources="allResourcesInScope"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<Banner :color="(matchingResources.none ? 'warning' : 'success')">
|
||||||
|
<span v-clean-html="t('generic.selectors.matchingResources.matchesSome', matchingResources)" />
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<ResourceTable
|
||||||
|
:rows="matchingResources.matches"
|
||||||
|
:headers="tableHeaders"
|
||||||
|
key-field="id"
|
||||||
|
:table-actions="false"
|
||||||
|
:row-actions="false"
|
||||||
|
:schema="schema"
|
||||||
|
:groupable="false"
|
||||||
|
:search="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -122,7 +122,7 @@ export default {
|
|||||||
return this.$store.getters['currentCluster'].isLocal;
|
return this.$store.getters['currentCluster'].isLocal;
|
||||||
},
|
},
|
||||||
canEditClusterMembers() {
|
canEditClusterMembers() {
|
||||||
return this.normanClusterRTBSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
|
return this.schema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -58,11 +58,14 @@ export default {
|
|||||||
|
|
||||||
const url = `https://${ host }${ prefix }/${ PRODUCT_NAME }/c/${ params.cluster }/console/${ uid }/${ type }`;
|
const url = `https://${ host }${ prefix }/${ PRODUCT_NAME }/c/${ params.cluster }/console/${ uid }/${ type }`;
|
||||||
|
|
||||||
|
// Defer so v-select can finish closing the dropdown before the popup steals focus
|
||||||
|
this.$nextTick(() => {
|
||||||
window.open(
|
window.open(
|
||||||
url,
|
url,
|
||||||
'_blank',
|
'_blank',
|
||||||
`toolbars=0,width=${ screen.width - 200 },height=${ screen.height - 200 },left=0,top=0,noreferrer`
|
`toolbars=0,width=${ screen.width - 200 },height=${ screen.height - 200 },left=0,top=0,noreferrer`
|
||||||
);
|
);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
isEmpty(o) {
|
isEmpty(o) {
|
||||||
|
|||||||
@ -0,0 +1,196 @@
|
|||||||
|
<script>
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { NAMESPACE } from '@shell/config/types';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterClusterPodSecurityStandard',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
RadioGroup,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
await this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE });
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let enabled = false;
|
||||||
|
let whitelistedNamespaces = [];
|
||||||
|
let privilegedNamespaces = [];
|
||||||
|
let restrictedNamespaces = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(this.value.value || this.value.default || '{}');
|
||||||
|
|
||||||
|
enabled = !!parsed.enabled;
|
||||||
|
whitelistedNamespaces = parsed.whitelistedNamespacesList ? parsed.whitelistedNamespacesList.split(',') : [];
|
||||||
|
privilegedNamespaces = parsed.privilegedNamespacesList ? parsed.privilegedNamespacesList.split(',') : [];
|
||||||
|
restrictedNamespaces = parsed.restrictedNamespacesList ? parsed.restrictedNamespacesList.split(',') : [];
|
||||||
|
} catch (e) {
|
||||||
|
enabled = false;
|
||||||
|
whitelistedNamespaces = [];
|
||||||
|
privilegedNamespaces = [];
|
||||||
|
restrictedNamespaces = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
whitelistedNamespaces,
|
||||||
|
privilegedNamespaces,
|
||||||
|
restrictedNamespaces,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
enabledOptions() {
|
||||||
|
return [
|
||||||
|
{ label: this.t('generic.enabled'), value: true },
|
||||||
|
{ label: this.t('generic.disabled'), value: false },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
allNamespaces() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return this.$store.getters[`${ inStore }/all`](NAMESPACE).filter((ns) => !ns.isSystem).map((ns) => ns.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
whitelistedOptions() {
|
||||||
|
const excluded = new Set([...this.privilegedNamespaces, ...this.restrictedNamespaces]);
|
||||||
|
|
||||||
|
return this.allNamespaces.filter((ns) => !excluded.has(ns));
|
||||||
|
},
|
||||||
|
|
||||||
|
privilegedOptions() {
|
||||||
|
const excluded = new Set([...this.whitelistedNamespaces, ...this.restrictedNamespaces]);
|
||||||
|
|
||||||
|
return this.allNamespaces.filter((ns) => !excluded.has(ns));
|
||||||
|
},
|
||||||
|
|
||||||
|
restrictedOptions() {
|
||||||
|
const excluded = new Set([...this.whitelistedNamespaces, ...this.privilegedNamespaces]);
|
||||||
|
|
||||||
|
return this.allNamespaces.filter((ns) => !excluded.has(ns));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
useDefault() {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(this.value.default || '{}');
|
||||||
|
|
||||||
|
this.enabled = !!parsed.enabled;
|
||||||
|
this.whitelistedNamespaces = parsed.whitelistedNamespacesList ? parsed.whitelistedNamespacesList.split(',') : [];
|
||||||
|
this.privilegedNamespaces = parsed.privilegedNamespacesList ? parsed.privilegedNamespacesList.split(',') : [];
|
||||||
|
this.restrictedNamespaces = parsed.restrictedNamespacesList ? parsed.restrictedNamespacesList.split(',') : [];
|
||||||
|
} catch (e) {
|
||||||
|
this.enabled = false;
|
||||||
|
this.whitelistedNamespaces = [];
|
||||||
|
this.privilegedNamespaces = [];
|
||||||
|
this.restrictedNamespaces = [];
|
||||||
|
}
|
||||||
|
this.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
onUpdateEnabled() {
|
||||||
|
if (!this.enabled) {
|
||||||
|
this.whitelistedNamespaces = [];
|
||||||
|
this.privilegedNamespaces = [];
|
||||||
|
this.restrictedNamespaces = [];
|
||||||
|
}
|
||||||
|
this.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateWhitelisted(selected) {
|
||||||
|
this.whitelistedNamespaces = selected;
|
||||||
|
this.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePrivileged(selected) {
|
||||||
|
this.privilegedNamespaces = selected;
|
||||||
|
this.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateRestricted(selected) {
|
||||||
|
this.restrictedNamespaces = selected;
|
||||||
|
this.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
this.value.value = JSON.stringify({
|
||||||
|
enabled: this.enabled,
|
||||||
|
whitelistedNamespacesList: this.whitelistedNamespaces.join(','),
|
||||||
|
privilegedNamespacesList: this.privilegedNamespaces.join(','),
|
||||||
|
restrictedNamespacesList: this.restrictedNamespaces.join(','),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="enabled"
|
||||||
|
name="enabled"
|
||||||
|
:options="enabledOptions"
|
||||||
|
@update:value="onUpdateEnabled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-if="enabled">
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="whitelistedNamespaces"
|
||||||
|
:label="t('harvester.setting.clusterPodSecurityStandard.whitelistedNamespaces.label')"
|
||||||
|
:options="whitelistedOptions"
|
||||||
|
:multiple="true"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="updateWhitelisted"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="privilegedNamespaces"
|
||||||
|
:label="t('harvester.setting.clusterPodSecurityStandard.privilegedNamespaces.label')"
|
||||||
|
:options="privilegedOptions"
|
||||||
|
:multiple="true"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="updatePrivileged"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="restrictedNamespaces"
|
||||||
|
:label="t('harvester.setting.clusterPodSecurityStandard.restrictedNamespaces.label')"
|
||||||
|
:options="restrictedOptions"
|
||||||
|
:multiple="true"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="updateRestricted"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -64,6 +64,9 @@ export default {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
parsedDefaultValue = JSON.parse(this.value.value);
|
parsedDefaultValue = JSON.parse(this.value.value);
|
||||||
|
if (typeof parsedDefaultValue.exclusiveVlan !== 'boolean') {
|
||||||
|
parsedDefaultValue.exclusiveVlan = false;
|
||||||
|
}
|
||||||
networkType = 'vlan' in parsedDefaultValue ? L2VLAN : UNTAGGED; // backend doesn't provide networkType, so we check if vlan is provided instead
|
networkType = 'vlan' in parsedDefaultValue ? L2VLAN : UNTAGGED; // backend doesn't provide networkType, so we check if vlan is provided instead
|
||||||
openVlan = true;
|
openVlan = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -72,7 +75,8 @@ export default {
|
|||||||
vlan: '',
|
vlan: '',
|
||||||
clusterNetwork: '',
|
clusterNetwork: '',
|
||||||
range: '',
|
range: '',
|
||||||
exclude: []
|
exclude: [],
|
||||||
|
exclusiveVlan: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const exclude = parsedDefaultValue?.exclude?.toString().split(',') || [];
|
const exclude = parsedDefaultValue?.exclude?.toString().split(',') || [];
|
||||||
@ -94,6 +98,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
showExclusiveVlan() {
|
||||||
|
return this.networkType === L2VLAN &&
|
||||||
|
Number(this.parsedDefaultValue.vlan) !== 1;
|
||||||
|
},
|
||||||
showVlan() {
|
showVlan() {
|
||||||
return this.networkType === L2VLAN;
|
return this.networkType === L2VLAN;
|
||||||
},
|
},
|
||||||
@ -132,6 +140,18 @@ export default {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
exclusiveVlanOptions() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: this.t('generic.enabled'),
|
||||||
|
value: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.t('generic.disabled'),
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@ -174,7 +194,8 @@ export default {
|
|||||||
vlan: '',
|
vlan: '',
|
||||||
clusterNetwork: '',
|
clusterNetwork: '',
|
||||||
range: '',
|
range: '',
|
||||||
exclude: []
|
exclude: [],
|
||||||
|
exclusiveVlan: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -280,7 +301,15 @@ export default {
|
|||||||
label-key="harvester.setting.storageNetwork.vlan"
|
label-key="harvester.setting.storageNetwork.vlan"
|
||||||
@update:value="inputVlan"
|
@update:value="inputVlan"
|
||||||
/>
|
/>
|
||||||
|
<LabeledSelect
|
||||||
|
v-if="showExclusiveVlan"
|
||||||
|
v-model:value="parsedDefaultValue.exclusiveVlan"
|
||||||
|
class="mb-20"
|
||||||
|
:options="exclusiveVlanOptions"
|
||||||
|
:mode="mode"
|
||||||
|
label-key="harvester.setting.storageNetwork.exclusiveVlan"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="parsedDefaultValue.clusterNetwork"
|
v-model:value="parsedDefaultValue.clusterNetwork"
|
||||||
label-key="harvester.setting.storageNetwork.clusterNetwork"
|
label-key="harvester.setting.storageNetwork.clusterNetwork"
|
||||||
|
|||||||
@ -65,7 +65,15 @@ const FEATURE_FLAGS = {
|
|||||||
'vGPUAsPCIDevice',
|
'vGPUAsPCIDevice',
|
||||||
'instanceManagerResourcesSetting',
|
'instanceManagerResourcesSetting',
|
||||||
'rwxNetworkSetting',
|
'rwxNetworkSetting',
|
||||||
'createPVCWithDataVolume'
|
'createPVCWithDataVolume',
|
||||||
|
'clusterPodSecurityStandardSetting',
|
||||||
|
],
|
||||||
|
'v1.8.1': [],
|
||||||
|
'v1.9.0': [
|
||||||
|
'supportFilesystem',
|
||||||
|
'disableResourcePooling',
|
||||||
|
'expandOnlineEncryptedVolume',
|
||||||
|
'longhornV2HugepageSettings'
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -54,6 +54,14 @@ import { registerAddonSideNav } from '../utils/dynamic-nav';
|
|||||||
const TEMPLATE = HCI.VM_VERSION;
|
const TEMPLATE = HCI.VM_VERSION;
|
||||||
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
|
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
|
||||||
const LOGGING_GROUP = 'Monitoring & Logging::Logging';
|
const LOGGING_GROUP = 'Monitoring & Logging::Logging';
|
||||||
|
const OVERLAY_NETWORKS_GROUP = 'Overlay Networks';
|
||||||
|
const UNDERLAY_NETWORKS_GROUP = 'Underlay Networks';
|
||||||
|
const NAT_INTERNET_GROUP = `${ OVERLAY_NETWORKS_GROUP }::NAT & Internet`;
|
||||||
|
const GATEWAYS_GROUP = `${ NAT_INTERNET_GROUP }::Gateways`;
|
||||||
|
const EXTERNAL_IPS_GROUP = `${ NAT_INTERNET_GROUP }::External IPs`;
|
||||||
|
const RULES_GROUP = `${ NAT_INTERNET_GROUP }::Rules`;
|
||||||
|
const SOURCE_RULES_GROUP = `${ RULES_GROUP }::Source Rules`;
|
||||||
|
const DESTINATION_RULES_GROUP = `${ RULES_GROUP }::Destination Rules`;
|
||||||
|
|
||||||
export const PRODUCT_NAME = 'harvester';
|
export const PRODUCT_NAME = 'harvester';
|
||||||
|
|
||||||
@ -237,6 +245,7 @@ export function init($plugin, store) {
|
|||||||
labelKey: 'harvester.addons.vmImport.labels.vmimport',
|
labelKey: 'harvester.addons.vmImport.labels.vmimport',
|
||||||
group: 'vmimport',
|
group: 'vmimport',
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
|
ifHaveType: HCI.VMIMPORT,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.VMIMPORT }
|
params: { resource: HCI.VMIMPORT }
|
||||||
@ -266,6 +275,7 @@ export function init($plugin, store) {
|
|||||||
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare',
|
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare',
|
||||||
group: 'vmimport',
|
group: 'vmimport',
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
|
ifHaveType: HCI.VMIMPORT_SOURCE_V,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.VMIMPORT_SOURCE_V }
|
params: { resource: HCI.VMIMPORT_SOURCE_V }
|
||||||
@ -295,6 +305,7 @@ export function init($plugin, store) {
|
|||||||
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack',
|
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack',
|
||||||
group: 'vmimport',
|
group: 'vmimport',
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
|
ifHaveType: HCI.VMIMPORT_SOURCE_O,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.VMIMPORT_SOURCE_O }
|
params: { resource: HCI.VMIMPORT_SOURCE_O }
|
||||||
@ -323,6 +334,7 @@ export function init($plugin, store) {
|
|||||||
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA',
|
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA',
|
||||||
group: 'vmimport',
|
group: 'vmimport',
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
|
ifHaveType: HCI.VMIMPORT_SOURCE_OVA,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.VMIMPORT_SOURCE_OVA }
|
params: { resource: HCI.VMIMPORT_SOURCE_OVA }
|
||||||
@ -484,6 +496,7 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
|
ifHaveType: LOGGING.CLUSTER_FLOW,
|
||||||
labelKey: 'harvester.logging.clusterFlow.label',
|
labelKey: 'harvester.logging.clusterFlow.label',
|
||||||
name: HCI.CLUSTER_FLOW,
|
name: HCI.CLUSTER_FLOW,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
@ -507,6 +520,7 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
|
ifHaveType: LOGGING.CLUSTER_OUTPUT,
|
||||||
labelKey: 'harvester.logging.clusterOutput.label',
|
labelKey: 'harvester.logging.clusterOutput.label',
|
||||||
name: HCI.CLUSTER_OUTPUT,
|
name: HCI.CLUSTER_OUTPUT,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
@ -530,6 +544,7 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
|
ifHaveType: LOGGING.FLOW,
|
||||||
labelKey: 'harvester.logging.flow.label',
|
labelKey: 'harvester.logging.flow.label',
|
||||||
name: HCI.FLOW,
|
name: HCI.FLOW,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
@ -553,6 +568,7 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
|
ifHaveType: LOGGING.OUTPUT,
|
||||||
labelKey: 'harvester.logging.output.label',
|
labelKey: 'harvester.logging.output.label',
|
||||||
name: HCI.OUTPUT,
|
name: HCI.OUTPUT,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
@ -573,14 +589,53 @@ export function init($plugin, store) {
|
|||||||
[
|
[
|
||||||
HCI.CLUSTER_NETWORK,
|
HCI.CLUSTER_NETWORK,
|
||||||
HCI.NETWORK_ATTACHMENT,
|
HCI.NETWORK_ATTACHMENT,
|
||||||
HCI.VPC,
|
HCI.HOST_NETWORK_CONFIG,
|
||||||
NETWORK_POLICY,
|
|
||||||
HCI.LB,
|
HCI.LB,
|
||||||
HCI.IP_POOL,
|
HCI.IP_POOL,
|
||||||
],
|
],
|
||||||
'networks'
|
'networks'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
basicType(
|
||||||
|
[HCI.VPC],
|
||||||
|
OVERLAY_NETWORKS_GROUP
|
||||||
|
);
|
||||||
|
|
||||||
|
basicType(
|
||||||
|
[NETWORK_POLICY],
|
||||||
|
OVERLAY_NETWORKS_GROUP
|
||||||
|
);
|
||||||
|
|
||||||
|
basicType(
|
||||||
|
[HCI.VPC_NAT_GATEWAY],
|
||||||
|
GATEWAYS_GROUP
|
||||||
|
);
|
||||||
|
|
||||||
|
basicType(
|
||||||
|
[HCI.IPTABLES_EIP],
|
||||||
|
EXTERNAL_IPS_GROUP
|
||||||
|
);
|
||||||
|
|
||||||
|
basicType(
|
||||||
|
[HCI.IPTABLES_SNAT_RULE],
|
||||||
|
SOURCE_RULES_GROUP
|
||||||
|
);
|
||||||
|
|
||||||
|
basicType(
|
||||||
|
[HCI.IPTABLES_DNAT_RULE],
|
||||||
|
DESTINATION_RULES_GROUP
|
||||||
|
);
|
||||||
|
|
||||||
|
basicType(
|
||||||
|
[HCI.PROVIDER_NETWORK],
|
||||||
|
UNDERLAY_NETWORKS_GROUP
|
||||||
|
);
|
||||||
|
|
||||||
|
basicType(
|
||||||
|
[HCI.VLAN],
|
||||||
|
UNDERLAY_NETWORKS_GROUP
|
||||||
|
);
|
||||||
|
|
||||||
basicType(
|
basicType(
|
||||||
[
|
[
|
||||||
HCI.SCHEDULE_VM_BACKUP,
|
HCI.SCHEDULE_VM_BACKUP,
|
||||||
@ -592,7 +647,11 @@ export function init($plugin, store) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
weightGroup('networks', 494, true);
|
weightGroup('networks', 494, true);
|
||||||
weightGroup('backupAndSnapshot', 493, true);
|
weightGroup('Overlay Networks', 493, true);
|
||||||
|
weightGroup('NAT & Internet', 492, true);
|
||||||
|
weightGroup('Rules', 491, true);
|
||||||
|
weightGroup('Underlay Networks', 490, true);
|
||||||
|
weightGroup('backupAndSnapshot', 489, true);
|
||||||
|
|
||||||
basicType(
|
basicType(
|
||||||
[
|
[
|
||||||
@ -671,7 +730,7 @@ export function init($plugin, store) {
|
|||||||
name: HCI.CLUSTER_NETWORK,
|
name: HCI.CLUSTER_NETWORK,
|
||||||
ifHaveType: HCI.CLUSTER_NETWORK,
|
ifHaveType: HCI.CLUSTER_NETWORK,
|
||||||
namespaced: false,
|
namespaced: false,
|
||||||
weight: 189,
|
weight: 484,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.CLUSTER_NETWORK }
|
params: { resource: HCI.CLUSTER_NETWORK }
|
||||||
@ -686,14 +745,14 @@ export function init($plugin, store) {
|
|||||||
},
|
},
|
||||||
resource: NETWORK_ATTACHMENT,
|
resource: NETWORK_ATTACHMENT,
|
||||||
resourceDetail: HCI.NETWORK_ATTACHMENT,
|
resourceDetail: HCI.NETWORK_ATTACHMENT,
|
||||||
resourceEdit: HCI.NETWORK_ATTACHMENT
|
resourceEdit: HCI.NETWORK_ATTACHMENT,
|
||||||
});
|
});
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
labelKey: 'harvester.network.label',
|
labelKey: 'harvester.network.label',
|
||||||
name: HCI.NETWORK_ATTACHMENT,
|
name: HCI.NETWORK_ATTACHMENT,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 188,
|
weight: 485,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.NETWORK_ATTACHMENT }
|
params: { resource: HCI.NETWORK_ATTACHMENT }
|
||||||
@ -707,7 +766,7 @@ export function init($plugin, store) {
|
|||||||
labelKey: 'harvester.vpc.label',
|
labelKey: 'harvester.vpc.label',
|
||||||
name: HCI.VPC,
|
name: HCI.VPC,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 187,
|
weight: 195,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.VPC }
|
params: { resource: HCI.VPC }
|
||||||
@ -716,13 +775,73 @@ export function init($plugin, store) {
|
|||||||
ifHaveType: HCI.VPC,
|
ifHaveType: HCI.VPC,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
configureType(HCI.VPC_NAT_GATEWAY, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||||
|
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.natGateway.label',
|
||||||
|
name: HCI.VPC_NAT_GATEWAY,
|
||||||
|
namespaced: false,
|
||||||
|
weight: 193,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VPC_NAT_GATEWAY }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.VPC_NAT_GATEWAY,
|
||||||
|
});
|
||||||
|
|
||||||
|
configureType(HCI.IPTABLES_EIP, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||||
|
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.externalIP.label',
|
||||||
|
name: HCI.IPTABLES_EIP,
|
||||||
|
namespaced: false,
|
||||||
|
weight: 192,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.IPTABLES_EIP }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.IPTABLES_EIP,
|
||||||
|
});
|
||||||
|
|
||||||
|
configureType(HCI.IPTABLES_SNAT_RULE, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||||
|
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.snat.label',
|
||||||
|
name: HCI.IPTABLES_SNAT_RULE,
|
||||||
|
namespaced: false,
|
||||||
|
weight: 191,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.IPTABLES_SNAT_RULE }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.IPTABLES_SNAT_RULE,
|
||||||
|
});
|
||||||
|
|
||||||
|
configureType(HCI.IPTABLES_DNAT_RULE, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||||
|
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.dnat.label',
|
||||||
|
name: HCI.IPTABLES_DNAT_RULE,
|
||||||
|
namespaced: false,
|
||||||
|
weight: 190,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.IPTABLES_DNAT_RULE }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.IPTABLES_DNAT_RULE,
|
||||||
|
});
|
||||||
|
|
||||||
configureType(NETWORK_POLICY, { hiddenNamespaceGroupButton: true, canYaml: false });
|
configureType(NETWORK_POLICY, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
labelKey: 'harvester.networkPolicy.label',
|
labelKey: 'harvester.networkPolicy.label',
|
||||||
name: NETWORK_POLICY,
|
name: NETWORK_POLICY,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 186,
|
weight: 194,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: NETWORK_POLICY }
|
params: { resource: NETWORK_POLICY }
|
||||||
@ -731,6 +850,53 @@ export function init($plugin, store) {
|
|||||||
ifHaveType: NETWORK_POLICY,
|
ifHaveType: NETWORK_POLICY,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
configureType(HCI.PROVIDER_NETWORK, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||||
|
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.providerNetwork.label',
|
||||||
|
name: HCI.PROVIDER_NETWORK,
|
||||||
|
namespaced: false,
|
||||||
|
weight: 189,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.PROVIDER_NETWORK }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.PROVIDER_NETWORK,
|
||||||
|
});
|
||||||
|
|
||||||
|
configureType(HCI.VLAN, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||||
|
|
||||||
|
headers(HCI.VLAN, [
|
||||||
|
STATE,
|
||||||
|
NAME_COL,
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
label: 'ID',
|
||||||
|
value: 'spec.id',
|
||||||
|
sort: 'spec.id'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'provider',
|
||||||
|
labelKey: 'harvester.subnet.provider.label',
|
||||||
|
value: 'spec.provider',
|
||||||
|
sort: 'spec.provider'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.vlanNetwork.label',
|
||||||
|
name: HCI.VLAN,
|
||||||
|
namespaced: false,
|
||||||
|
weight: 188,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VLAN }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.VLAN,
|
||||||
|
});
|
||||||
|
|
||||||
configureType(HCI.SNAPSHOT, {
|
configureType(HCI.SNAPSHOT, {
|
||||||
isCreatable: false,
|
isCreatable: false,
|
||||||
location: {
|
location: {
|
||||||
@ -1085,7 +1251,7 @@ export function init($plugin, store) {
|
|||||||
labelKey: 'harvester.loadBalancer.label',
|
labelKey: 'harvester.loadBalancer.label',
|
||||||
name: HCI.LB,
|
name: HCI.LB,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 185,
|
weight: 483,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.LB }
|
params: { resource: HCI.LB }
|
||||||
@ -1124,7 +1290,7 @@ export function init($plugin, store) {
|
|||||||
labelKey: 'harvester.ipPool.label',
|
labelKey: 'harvester.ipPool.label',
|
||||||
name: HCI.IP_POOL,
|
name: HCI.IP_POOL,
|
||||||
namespaced: false,
|
namespaced: false,
|
||||||
weight: 184,
|
weight: 482,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.IP_POOL }
|
params: { resource: HCI.IP_POOL }
|
||||||
@ -1133,4 +1299,24 @@ export function init($plugin, store) {
|
|||||||
ifHaveType: HCI.IP_POOL,
|
ifHaveType: HCI.IP_POOL,
|
||||||
});
|
});
|
||||||
headers(HCI.IP_POOL, IP_POOL_HEADERS);
|
headers(HCI.IP_POOL, IP_POOL_HEADERS);
|
||||||
|
|
||||||
|
configureType(HCI.HOST_NETWORK_CONFIG, {
|
||||||
|
location: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.HOST_NETWORK_CONFIG }
|
||||||
|
},
|
||||||
|
canYaml: false,
|
||||||
|
});
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.hostNetworkConfig.label',
|
||||||
|
name: HCI.HOST_NETWORK_CONFIG,
|
||||||
|
namespaced: false,
|
||||||
|
weight: 481,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.HOST_NETWORK_CONFIG }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.HOST_NETWORK_CONFIG,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -88,6 +88,8 @@ export const CSI_SECRETS = {
|
|||||||
CSI_NODE_PUBLISH_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-publish-secret-namespace',
|
CSI_NODE_PUBLISH_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-publish-secret-namespace',
|
||||||
CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name',
|
CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name',
|
||||||
CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace',
|
CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace',
|
||||||
|
CSI_NODE_EXPAND_SECRET_NAME: 'csi.storage.k8s.io/node-expand-secret-name',
|
||||||
|
CSI_NODE_EXPAND_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-expand-secret-namespace'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Some harvester CRD type is not equal to model file name, define the mapping here
|
// Some harvester CRD type is not equal to model file name, define the mapping here
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export const HCI = {
|
|||||||
NETWORK_TYPE: 'network.harvesterhci.io/type',
|
NETWORK_TYPE: 'network.harvesterhci.io/type',
|
||||||
VM_NAME: 'harvesterhci.io/vmName',
|
VM_NAME: 'harvesterhci.io/vmName',
|
||||||
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
|
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
|
||||||
|
VM_DISPLAY_NAME: 'harvesterhci.io/vmDisplayName',
|
||||||
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
|
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
|
||||||
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
|
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
|
||||||
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
|
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
|
||||||
@ -81,4 +82,5 @@ export const HCI = {
|
|||||||
MAC_ADDRESS: 'harvesterhci.io/mac-address',
|
MAC_ADDRESS: 'harvesterhci.io/mac-address',
|
||||||
NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map',
|
NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map',
|
||||||
CDI_POPULATOR_KIND: 'cdi.kubevirt.io/storage.populator.kind',
|
CDI_POPULATOR_KIND: 'cdi.kubevirt.io/storage.populator.kind',
|
||||||
|
CNI_NETWORKS: 'k8s.v1.cni.cncf.io/networks',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -35,13 +35,16 @@ export const HCI_SETTING = {
|
|||||||
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
|
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
|
||||||
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
|
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
|
||||||
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
|
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
|
||||||
|
LONGHORN_V2_DATA_ENGINE_HUGEPAGE_ENABLED: 'longhorn-v2-data-engine-hugepage-enabled',
|
||||||
|
LONGHORN_V2_DATA_ENGINE_MEMORY_SIZE: 'longhorn-v2-data-engine-memory-size',
|
||||||
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
|
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
|
||||||
UPGRADE_CONFIG: 'upgrade-config',
|
UPGRADE_CONFIG: 'upgrade-config',
|
||||||
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
||||||
RANCHER_CLUSTER: 'rancher-cluster',
|
RANCHER_CLUSTER: 'rancher-cluster',
|
||||||
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
|
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
|
||||||
KUBEVIRT_MIGRATION: 'kubevirt-migration',
|
KUBEVIRT_MIGRATION: 'kubevirt-migration',
|
||||||
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources'
|
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources',
|
||||||
|
CLUSTER_POD_SECURITY_STANDARD: 'cluster-pod-security-standard'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_ALLOWED_SETTINGS = {
|
export const HCI_ALLOWED_SETTINGS = {
|
||||||
@ -111,6 +114,14 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
experimental: true,
|
experimental: true,
|
||||||
featureFlag: 'longhornV2LVMSupport'
|
featureFlag: 'longhornV2LVMSupport'
|
||||||
},
|
},
|
||||||
|
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_HUGEPAGE_ENABLED]: {
|
||||||
|
kind: 'boolean',
|
||||||
|
featureFlag: 'longhornV2HugepageSettings'
|
||||||
|
},
|
||||||
|
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_MEMORY_SIZE]: {
|
||||||
|
kind: 'number',
|
||||||
|
featureFlag: 'longhornV2HugepageSettings'
|
||||||
|
},
|
||||||
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
|
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
|
||||||
[HCI_SETTING.UPGRADE_CONFIG]: {
|
[HCI_SETTING.UPGRADE_CONFIG]: {
|
||||||
kind: 'json',
|
kind: 'json',
|
||||||
@ -130,6 +141,9 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
},
|
},
|
||||||
[HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: {
|
[HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: {
|
||||||
kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting'
|
kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting'
|
||||||
|
},
|
||||||
|
[HCI_SETTING.CLUSTER_POD_SECURITY_STANDARD]: {
|
||||||
|
kind: 'json', from: 'import', canReset: true, featureFlag: 'clusterPodSecurityStandardSetting'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -46,3 +46,9 @@ export const CDI_POPULATOR_KIND = {
|
|||||||
VOLUME_IMPORT_SOURCE: 'VolumeImportSource',
|
VOLUME_IMPORT_SOURCE: 'VolumeImportSource',
|
||||||
VOLUME_CLONE_SOURCE: 'VolumeCloneSource',
|
VOLUME_CLONE_SOURCE: 'VolumeCloneSource',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const FILESYSTEM_SOURCE_TYPE = {
|
||||||
|
CONFIGMAP: 'configmap',
|
||||||
|
SECRET: 'secret',
|
||||||
|
SERVICEACCOUNT: 'serviceaccount',
|
||||||
|
};
|
||||||
|
|||||||
@ -27,15 +27,11 @@ export default {
|
|||||||
await allHash({
|
await allHash({
|
||||||
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
|
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
|
||||||
vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }),
|
vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }),
|
||||||
allClusterNetwork: this.$store.dispatch('harvester/findAll', { type: HCI.CLUSTER_NETWORK }),
|
vmims: this.$store.dispatch('harvester/findAll', { type: HCI.VMIM }),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
allClusterNetwork() {
|
|
||||||
return this.$store.getters['harvester/all'](HCI.CLUSTER_NETWORK);
|
|
||||||
},
|
|
||||||
|
|
||||||
rows() {
|
rows() {
|
||||||
const vms = this.$store.getters['harvester/all'](HCI.VM);
|
const vms = this.$store.getters['harvester/all'](HCI.VM);
|
||||||
|
|
||||||
@ -108,7 +104,6 @@ export default {
|
|||||||
<HarvesterVmState
|
<HarvesterVmState
|
||||||
class="vmstate"
|
class="vmstate"
|
||||||
:row="scope.row"
|
:row="scope.row"
|
||||||
:all-cluster-network="allClusterNetwork"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -98,9 +98,9 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
|
|
||||||
close() {
|
close(data) {
|
||||||
this.errors = [];
|
this.errors = [];
|
||||||
this.$emit('close');
|
this.$emit('close', data);
|
||||||
},
|
},
|
||||||
|
|
||||||
async apply(buttonDone) {
|
async apply(buttonDone) {
|
||||||
@ -109,7 +109,7 @@ export default {
|
|||||||
await resource.doActionGrowl(this.modalData.action, {});
|
await resource.doActionGrowl(this.modalData.action, {});
|
||||||
}
|
}
|
||||||
buttonDone(true);
|
buttonDone(true);
|
||||||
this.close();
|
this.close({ performCallback: true, clearTableSelection: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errors = exceptionToErrorsArray(e);
|
this.errors = exceptionToErrorsArray(e);
|
||||||
buttonDone(false);
|
buttonDone(false);
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
import { Card } from '@components/Card';
|
import { Card } from '@components/Card';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
import { escapeHtml } from '@shell/utils/string';
|
import { escapeHtml } from '@shell/utils/string';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
@ -13,7 +15,9 @@ export default {
|
|||||||
|
|
||||||
components: {
|
components: {
|
||||||
AsyncButton,
|
AsyncButton,
|
||||||
|
Banner,
|
||||||
Card,
|
Card,
|
||||||
|
Checkbox,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@ -24,10 +28,16 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {};
|
return { disableResourcePooling: false };
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: { ...mapGetters({ t: 'i18n/t' }) },
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
disableResourcePoolingEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('disableResourcePooling');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
close() {
|
close() {
|
||||||
@ -54,7 +64,8 @@ export default {
|
|||||||
spec: {
|
spec: {
|
||||||
address: actionResource.status.address,
|
address: actionResource.status.address,
|
||||||
nodeName: actionResource.status.nodeName,
|
nodeName: actionResource.status.nodeName,
|
||||||
userName
|
userName,
|
||||||
|
disableResourcePooling: this.disableResourcePooling,
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
@ -85,7 +96,19 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body>
|
<template #body>
|
||||||
|
<p class="mb-20">
|
||||||
{{ t('harvester.pci.enablePassthroughWarning') }}
|
{{ t('harvester.pci.enablePassthroughWarning') }}
|
||||||
|
</p>
|
||||||
|
<template v-if="disableResourcePoolingEnabled">
|
||||||
|
<Checkbox
|
||||||
|
v-model:value="disableResourcePooling"
|
||||||
|
label-key="harvester.pci.disableResourcePooling"
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
color="info"
|
||||||
|
:label="t('harvester.pci.disableResourcePoolingDescription')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
|
|||||||
154
pkg/harvester/dialog/HarvesterEnableNvidiaDriverToolkit.vue
Normal file
154
pkg/harvester/dialog/HarvesterEnableNvidiaDriverToolkit.vue
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
<script>
|
||||||
|
import merge from 'lodash/merge';
|
||||||
|
import jsyaml from 'js-yaml';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { escapeHtml } from '@shell/utils/string';
|
||||||
|
|
||||||
|
const DEFAULT_VALUE = { image: { repository: 'rancher/harvester-nvidia-driver-toolkit' } };
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterEnableNvidiaDriverToolkit',
|
||||||
|
|
||||||
|
emits: ['close'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton,
|
||||||
|
Card,
|
||||||
|
LabeledInput,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const addon = this.resources[0];
|
||||||
|
let valuesContentJson;
|
||||||
|
|
||||||
|
try {
|
||||||
|
valuesContentJson = merge({}, DEFAULT_VALUE, jsyaml.load(addon.spec.valuesContent));
|
||||||
|
} catch (e) {
|
||||||
|
valuesContentJson = { ...DEFAULT_VALUE };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valuesContentJson };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
buttonDisabled() {
|
||||||
|
const { image, driverLocation } = this.valuesContentJson;
|
||||||
|
|
||||||
|
return !(image?.repository || '').trim() || !(image?.tag || '').trim() || !(driverLocation || '').trim();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async enable(buttonCb) {
|
||||||
|
const addon = this.resources[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
addon.spec.valuesContent = jsyaml.dump(this.valuesContentJson);
|
||||||
|
addon.spec.enabled = true;
|
||||||
|
await addon.save();
|
||||||
|
buttonCb(true);
|
||||||
|
this.close();
|
||||||
|
} catch (err) {
|
||||||
|
addon.spec.enabled = false;
|
||||||
|
this.$store.dispatch('growl/fromError', {
|
||||||
|
title: this.t('generic.notification.title.error', { name: escapeHtml(addon.metadata.name) }),
|
||||||
|
err,
|
||||||
|
}, { root: true });
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<template #title>
|
||||||
|
<h4
|
||||||
|
v-clean-html="t('harvester.addons.nvidiaDriverToolkit.enable.title')"
|
||||||
|
class="text-default-text"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<div class="body">
|
||||||
|
<div class="row mb-15">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="valuesContentJson.image.repository"
|
||||||
|
:required="true"
|
||||||
|
:label="t('harvester.addons.nvidiaDriverToolkit.image.repository')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="valuesContentJson.image.tag"
|
||||||
|
:required="true"
|
||||||
|
:label="t('harvester.addons.nvidiaDriverToolkit.image.tag')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-15">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="valuesContentJson.driverLocation"
|
||||||
|
:required="true"
|
||||||
|
:label="t('harvester.addons.nvidiaDriverToolkit.driver.location')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<div class="buttons actions">
|
||||||
|
<button
|
||||||
|
class="btn role-secondary mr-10"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
mode="enable"
|
||||||
|
:disabled="buttonDisabled"
|
||||||
|
@click="enable"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,15 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
import { NODE } from '@shell/config/types';
|
import { NODE } from '@shell/config/types';
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
|
||||||
import { Card } from '@components/Card';
|
import { Card } from '@components/Card';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { HCI } from '../types';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
@ -62,28 +59,46 @@ export default {
|
|||||||
return this.resources[0];
|
return this.resources[0];
|
||||||
},
|
},
|
||||||
|
|
||||||
vmi() {
|
anyCpuPinning() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
return this.resources.some((r) => r.isCpuPinning);
|
||||||
const vmiResources = this.$store.getters[`${ inStore }/all`](HCI.VMI);
|
},
|
||||||
const resource = vmiResources.find((VMI) => VMI.id === this.actionResource?.id) || null;
|
|
||||||
|
|
||||||
return resource;
|
vmsByNode() {
|
||||||
|
const groups = {};
|
||||||
|
|
||||||
|
for (const r of this.resources) {
|
||||||
|
const node = r.nodeName || '';
|
||||||
|
const name = r.nameDisplay || r.name || r.id;
|
||||||
|
|
||||||
|
if (!groups[node]) {
|
||||||
|
groups[node] = [];
|
||||||
|
}
|
||||||
|
groups[node].push(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(groups).map(([node, vms]) => ({ node, vms })).sort((a, b) => a.node.localeCompare(b.node));
|
||||||
},
|
},
|
||||||
|
|
||||||
cpuPinningAlertMessage() {
|
cpuPinningAlertMessage() {
|
||||||
return this.t('harvester.virtualMachine.cpuPinning.migrationMessage');
|
return this.t('harvester.virtualMachine.cpuPinning.migrationMessage');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
allVmsOnTargetNode() {
|
||||||
|
if (!this.nodeName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.resources.every((r) => r.nodeName === this.nodeName);
|
||||||
|
},
|
||||||
|
|
||||||
nodeNameList() {
|
nodeNameList() {
|
||||||
const nodes = this.$store.getters['harvester/all'](NODE);
|
const nodes = this.$store.getters['harvester/all'](NODE);
|
||||||
|
|
||||||
return nodes.filter((n) => {
|
return nodes.filter((n) => {
|
||||||
const isNotSelfNode = !!this.availableNodes.includes(n.id);
|
|
||||||
const isNotWitnessNode = n.isEtcd !== 'true'; // do not allow to migrate to self node and witness node
|
const isNotWitnessNode = n.isEtcd !== 'true'; // do not allow to migrate to self node and witness node
|
||||||
const isCpuPinning = this.actionResource?.isCpuPinning;
|
const matchingCpuManagerConfig = !this.anyCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
|
||||||
const matchingCpuManagerConfig = !isCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
|
|
||||||
|
|
||||||
return isNotSelfNode && isNotWitnessNode && matchingCpuManagerConfig;
|
return isNotWitnessNode && matchingCpuManagerConfig;
|
||||||
}).map((n) => {
|
}).map((n) => {
|
||||||
let label = n?.metadata?.name;
|
let label = n?.metadata?.name;
|
||||||
const value = n?.metadata?.name;
|
const value = n?.metadata?.name;
|
||||||
@ -102,10 +117,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
close() {
|
close(data) {
|
||||||
this.nodeName = '';
|
this.nodeName = '';
|
||||||
this.errors = [];
|
this.errors = [];
|
||||||
this.$emit('close');
|
this.$emit('close', data);
|
||||||
},
|
},
|
||||||
|
|
||||||
async apply(buttonDone) {
|
async apply(buttonDone) {
|
||||||
@ -126,10 +141,32 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.actionResource.doAction('migrate', { nodeName: this.nodeName }, {}, false);
|
// Filter out VMs already running on the selected node
|
||||||
|
const toMigrate = this.resources.filter((r) => r.nodeName !== this.nodeName);
|
||||||
|
|
||||||
|
// await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
|
||||||
|
// We want to show all migration errors if there are multiple VMs, so we use allSettled here and handle the results accordingly.
|
||||||
|
const results = await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
|
||||||
|
|
||||||
|
const failedMigrations = results
|
||||||
|
.map((result, index) => ({ resource: toMigrate[index], result }))
|
||||||
|
.filter(({ result }) => result.status === 'rejected');
|
||||||
|
|
||||||
|
if (failedMigrations.length) {
|
||||||
|
this['errors'] = failedMigrations.flatMap(({ resource, result }) => {
|
||||||
|
const vmName = resource?.nameDisplay || resource?.name || resource?.metadata?.name || this.$store.getters['i18n/t']('generic.unknown');
|
||||||
|
const error = result.reason?.data || result.reason;
|
||||||
|
const messages = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
|
return messages.map((message) => `${ vmName }: ${ message }`);
|
||||||
|
});
|
||||||
|
buttonDone(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
buttonDone(true);
|
buttonDone(true);
|
||||||
this.close();
|
this.close({ performCallback: true, clearTableSelection: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err?.data || err;
|
const error = err?.data || err;
|
||||||
const message = exceptionToErrorsArray(error);
|
const message = exceptionToErrorsArray(error);
|
||||||
@ -146,17 +183,35 @@ export default {
|
|||||||
<template>
|
<template>
|
||||||
<Card :show-highlight-border="false">
|
<Card :show-highlight-border="false">
|
||||||
<template #title>
|
<template #title>
|
||||||
{{ t('harvester.modal.migration.title') }}
|
{{ t('harvester.modal.migration.vmMigrationTitle', { count: resources.length }) }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body>
|
<template #body>
|
||||||
<Banner
|
<Banner
|
||||||
v-if="actionResource?.isCpuPinning"
|
v-if="anyCpuPinning"
|
||||||
color="warning"
|
color="warning"
|
||||||
:label="cpuPinningAlertMessage"
|
:label="cpuPinningAlertMessage"
|
||||||
/>
|
/>
|
||||||
|
<p>
|
||||||
|
{{ t('harvester.modal.migration.selectedVMs') }}
|
||||||
|
</p>
|
||||||
|
<ul class="vm-list">
|
||||||
|
<li
|
||||||
|
v-for="group in vmsByNode"
|
||||||
|
:key="group.node"
|
||||||
|
>
|
||||||
|
{{ group.node || t('harvester.modal.migration.unknownNode') }}: {{ group.vms.join(', ') }}
|
||||||
|
<span
|
||||||
|
v-if="nodeName && group.node === nodeName"
|
||||||
|
class="already-on-target"
|
||||||
|
>
|
||||||
|
({{ t('harvester.modal.migration.alreadyOnTarget') }})
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="nodeName"
|
v-model:value="nodeName"
|
||||||
|
class="mt-15"
|
||||||
:label="t('harvester.modal.migration.fields.nodeName.label')"
|
:label="t('harvester.modal.migration.fields.nodeName.label')"
|
||||||
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
|
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
|
||||||
:options="nodeNameList"
|
:options="nodeNameList"
|
||||||
@ -183,7 +238,7 @@ export default {
|
|||||||
|
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
mode="apply"
|
mode="apply"
|
||||||
:disabled="!nodeName"
|
:disabled="!nodeName || allVmsOnTargetNode"
|
||||||
@click="apply"
|
@click="apply"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -201,4 +256,16 @@ export default {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.already-on-target {
|
||||||
|
color: var(--warning);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-list {
|
||||||
|
list-style: disc;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -108,6 +108,7 @@ export default {
|
|||||||
<YamlEditor
|
<YamlEditor
|
||||||
ref="yamlUser"
|
ref="yamlUser"
|
||||||
v-model:value="config"
|
v-model:value="config"
|
||||||
|
:mode="mode"
|
||||||
class="yaml-editor"
|
class="yaml-editor"
|
||||||
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
|
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
|
||||||
@onChanges="update"
|
@onChanges="update"
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import FileSelector, { createOnSelected } from '@shell/components/form/FileSelec
|
|||||||
|
|
||||||
import { randomStr } from '@shell/utils/string';
|
import { randomStr } from '@shell/utils/string';
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { getLoginAwareErrors } from '../utils/error';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditKeypair',
|
name: 'HarvesterEditKeypair',
|
||||||
@ -63,6 +64,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
normalizedErrors() {
|
||||||
|
const message = this.t('harvester.virtualMachine.genericLoginError');
|
||||||
|
|
||||||
|
return getLoginAwareErrors(this.errors, message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: { onKeySelected: createOnSelected('publicKey') },
|
methods: { onKeySelected: createOnSelected('publicKey') },
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -72,10 +81,9 @@ export default {
|
|||||||
:done-route="doneRoute"
|
:done-route="doneRoute"
|
||||||
:resource="value"
|
:resource="value"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:errors="errors"
|
:errors="normalizedErrors"
|
||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
>
|
||||||
<div class="header mb-20">
|
<div class="header mb-20">
|
||||||
<FileSelector
|
<FileSelector
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|||||||
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { NAMESPACE, NODE } from '@shell/config/types';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import { NETWORK_TYPE, L2VLAN_MODE } from '../config/types';
|
import { NETWORK_TYPE, L2VLAN_MODE } from '../config/types';
|
||||||
import { removeObject } from '@shell/utils/array';
|
import { removeObject } from '@shell/utils/array';
|
||||||
@ -20,6 +21,7 @@ const { ACCESS, TRUNK } = L2VLAN_MODE;
|
|||||||
|
|
||||||
const AUTO = 'auto';
|
const AUTO = 'auto';
|
||||||
const MANUAL = 'manual';
|
const MANUAL = 'manual';
|
||||||
|
const KUBE_SYSTEM = 'kube-system';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ['update:value'],
|
emits: ['update:value'],
|
||||||
@ -70,7 +72,12 @@ export default {
|
|||||||
async fetch() {
|
async fetch() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
await allHash({ clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }) });
|
await allHash({
|
||||||
|
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
|
||||||
|
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
|
||||||
|
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
|
||||||
|
linkMonitors: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.LINK_MONITOR }),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
@ -199,6 +206,80 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.type === UNTAGGED;
|
return this.type === UNTAGGED;
|
||||||
|
},
|
||||||
|
|
||||||
|
showNicsTab() {
|
||||||
|
return this.isOverlayNetwork && this.value.metadata.namespace === KUBE_SYSTEM;
|
||||||
|
},
|
||||||
|
|
||||||
|
namespaceOptions() {
|
||||||
|
const ns = this.$store.getters['harvester/all'](NAMESPACE) || [];
|
||||||
|
|
||||||
|
// Allow users to select the "kube-system" namespace as the external subnet from Kube-OVN.
|
||||||
|
// This expects the provider network to be in the "kube-system" namespace for VPC NAT gateway functionality.
|
||||||
|
return ns
|
||||||
|
.filter((ns) => !ns.isSystem || ns.id === KUBE_SYSTEM)
|
||||||
|
.map((ns) => ({ name: ns.id }))
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
},
|
||||||
|
|
||||||
|
nodes() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const nodes = this.$store.getters[`${ inStore }/all`](NODE);
|
||||||
|
|
||||||
|
return nodes.filter((n) => n.isEtcd !== 'true');
|
||||||
|
},
|
||||||
|
|
||||||
|
nics() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const linkMonitor = this.$store.getters[`${ inStore }/byId`](HCI.LINK_MONITOR, 'nic') || {};
|
||||||
|
const linkStatus = linkMonitor?.status?.linkStatus || {};
|
||||||
|
const nodes = this.nodes.map((n) => n.id);
|
||||||
|
|
||||||
|
const out = [];
|
||||||
|
|
||||||
|
// Collect all nics from all nodes (for overlay, we select from all nodes)
|
||||||
|
Object.keys(linkStatus).map((nodeName) => {
|
||||||
|
if (nodes.includes(nodeName)) {
|
||||||
|
const nics = linkStatus[nodeName] || [];
|
||||||
|
|
||||||
|
nics.map((nic) => {
|
||||||
|
out.push({
|
||||||
|
...nic,
|
||||||
|
nodeName,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
nicOptions() {
|
||||||
|
const out = [];
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
(this.nics || []).forEach((nic) => {
|
||||||
|
if (!seen.has(nic.name)) {
|
||||||
|
seen.add(nic.name);
|
||||||
|
out.push({
|
||||||
|
label: nic.name,
|
||||||
|
value: nic.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return out.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
},
|
||||||
|
|
||||||
|
master: {
|
||||||
|
get() {
|
||||||
|
return this.config?.master || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
this.config.master = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -214,6 +295,7 @@ export default {
|
|||||||
this.config.ipam = {};
|
this.config.ipam = {};
|
||||||
this.config.bridge = '';
|
this.config.bridge = '';
|
||||||
delete this.config.provider;
|
delete this.config.provider;
|
||||||
|
delete this.config.master;
|
||||||
delete this.config.server_socket;
|
delete this.config.server_socket;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -230,6 +312,13 @@ export default {
|
|||||||
this.config.vlan = '';
|
this.config.vlan = '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'value.metadata.namespace'(newNamespace) {
|
||||||
|
// NIC selection is only valid for overlay in kube-system namespace.
|
||||||
|
if (newNamespace !== KUBE_SYSTEM) {
|
||||||
|
delete this.config.master;
|
||||||
|
this.value.spec.config = JSON.stringify({ ...this.config });
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@ -324,6 +413,10 @@ export default {
|
|||||||
delete this.config.promiscMode;
|
delete this.config.promiscMode;
|
||||||
delete this.config.vlan;
|
delete this.config.vlan;
|
||||||
delete this.config.ipam;
|
delete this.config.ipam;
|
||||||
|
|
||||||
|
if (this.value.metadata.namespace !== KUBE_SYSTEM) {
|
||||||
|
delete this.config.master;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isUntaggedNetwork) {
|
if (this.isUntaggedNetwork) {
|
||||||
@ -350,6 +443,7 @@ export default {
|
|||||||
ref="nd"
|
ref="nd"
|
||||||
:value="value"
|
:value="value"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
|
:namespace-options="namespaceOptions"
|
||||||
@update:value="$emit('update:value', $event)"
|
@update:value="$emit('update:value', $event)"
|
||||||
/>
|
/>
|
||||||
<Tabbed
|
<Tabbed
|
||||||
@ -521,6 +615,25 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
v-if="showNicsTab"
|
||||||
|
name="nics"
|
||||||
|
:label="t('harvester.network.tabs.nic')"
|
||||||
|
:weight="97"
|
||||||
|
class="bordered-table"
|
||||||
|
>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="master"
|
||||||
|
:label="t('harvester.vlanConfig.uplink.nics.label')"
|
||||||
|
:placeholder="t('harvester.vlanConfig.uplink.nics.overlayWarning')"
|
||||||
|
:mode="mode"
|
||||||
|
:options="nicOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
</Tabbed>
|
</Tabbed>
|
||||||
</CruResource>
|
</CruResource>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -3,8 +3,9 @@ import KeyValue from '@shell/components/form/KeyValue';
|
|||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import RadioGroup from '@components/Form/Radio/RadioGroup';
|
import RadioGroup from '@components/Form/Radio/RadioGroup';
|
||||||
|
import Checkbox from '@components/Form/Checkbox/Checkbox';
|
||||||
import { SECRET, LONGHORN } from '@shell/config/types';
|
import { SECRET, LONGHORN } from '@shell/config/types';
|
||||||
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
import { _CREATE, _VIEW, _EDIT } from '@shell/config/query-params';
|
||||||
import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map';
|
import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map';
|
||||||
import { clone } from '@shell/utils/object';
|
import { clone } from '@shell/utils/object';
|
||||||
import { uniq } from '@shell/utils/array';
|
import { uniq } from '@shell/utils/array';
|
||||||
@ -16,7 +17,9 @@ const {
|
|||||||
CSI_NODE_PUBLISH_SECRET_NAME,
|
CSI_NODE_PUBLISH_SECRET_NAME,
|
||||||
CSI_NODE_PUBLISH_SECRET_NAMESPACE,
|
CSI_NODE_PUBLISH_SECRET_NAMESPACE,
|
||||||
CSI_NODE_STAGE_SECRET_NAME,
|
CSI_NODE_STAGE_SECRET_NAME,
|
||||||
CSI_NODE_STAGE_SECRET_NAMESPACE
|
CSI_NODE_STAGE_SECRET_NAMESPACE,
|
||||||
|
CSI_NODE_EXPAND_SECRET_NAME,
|
||||||
|
CSI_NODE_EXPAND_SECRET_NAMESPACE
|
||||||
} = CSI_SECRETS;
|
} = CSI_SECRETS;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -27,6 +30,7 @@ export default {
|
|||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
LabeledInput,
|
LabeledInput,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
|
Checkbox,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@ -57,7 +61,10 @@ export default {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { };
|
const hasExpandSecret = !!(this.value.parameters?.[CSI_NODE_EXPAND_SECRET_NAME] && this.value.parameters?.[CSI_NODE_EXPAND_SECRET_NAMESPACE]);
|
||||||
|
const volumeExpansionCheckBoxEnabled = this.realMode === _CREATE ? true : hasExpandSecret;
|
||||||
|
|
||||||
|
return { volumeExpansionCheckBoxEnabled };
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
@ -98,8 +105,11 @@ export default {
|
|||||||
}, []);
|
}, []);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isEdit() {
|
||||||
|
return this.realMode === _EDIT;
|
||||||
|
},
|
||||||
isView() {
|
isView() {
|
||||||
return this.mode === _VIEW;
|
return this.realMode === _VIEW;
|
||||||
},
|
},
|
||||||
|
|
||||||
migratableOptions() {
|
migratableOptions() {
|
||||||
@ -152,6 +162,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
enableOnlineExpansionVolumeEncryption() {
|
||||||
|
return this.value.expandOnlineEncryptedVolumeFeatureEnabled;
|
||||||
|
},
|
||||||
|
|
||||||
volumeEncryption: {
|
volumeEncryption: {
|
||||||
set(neu) {
|
set(neu) {
|
||||||
this.value['parameters'] = {
|
this.value['parameters'] = {
|
||||||
@ -180,6 +194,11 @@ export default {
|
|||||||
set(selectedSecret) {
|
set(selectedSecret) {
|
||||||
const [namespace, name] = selectedSecret.split('/');
|
const [namespace, name] = selectedSecret.split('/');
|
||||||
|
|
||||||
|
const expandSecretParams = (this.enableOnlineExpansionVolumeEncryption && this.volumeExpansionCheckBoxEnabled) ? {
|
||||||
|
[CSI_NODE_EXPAND_SECRET_NAME]: name,
|
||||||
|
[CSI_NODE_EXPAND_SECRET_NAMESPACE]: namespace,
|
||||||
|
} : {};
|
||||||
|
|
||||||
this.value['parameters'] = {
|
this.value['parameters'] = {
|
||||||
...this.value.parameters,
|
...this.value.parameters,
|
||||||
[CSI_PROVISIONER_SECRET_NAME]: name,
|
[CSI_PROVISIONER_SECRET_NAME]: name,
|
||||||
@ -187,7 +206,8 @@ export default {
|
|||||||
[CSI_NODE_STAGE_SECRET_NAME]: name,
|
[CSI_NODE_STAGE_SECRET_NAME]: name,
|
||||||
[CSI_PROVISIONER_SECRET_NAMESPACE]: namespace,
|
[CSI_PROVISIONER_SECRET_NAMESPACE]: namespace,
|
||||||
[CSI_NODE_PUBLISH_SECRET_NAMESPACE]: namespace,
|
[CSI_NODE_PUBLISH_SECRET_NAMESPACE]: namespace,
|
||||||
[CSI_NODE_STAGE_SECRET_NAMESPACE]: namespace
|
[CSI_NODE_STAGE_SECRET_NAMESPACE]: namespace,
|
||||||
|
...expandSecretParams,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -240,6 +260,32 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
volumeExpansionCheckBoxEnabled(enabled) {
|
||||||
|
const currentSecret = this.secret;
|
||||||
|
|
||||||
|
if (!currentSecret) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [namespace, name] = currentSecret.split('/');
|
||||||
|
|
||||||
|
if (enabled && this.enableOnlineExpansionVolumeEncryption) {
|
||||||
|
this.value['parameters'] = {
|
||||||
|
...this.value.parameters,
|
||||||
|
[CSI_NODE_EXPAND_SECRET_NAME]: name,
|
||||||
|
[CSI_NODE_EXPAND_SECRET_NAMESPACE]: namespace,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const params = { ...this.value.parameters };
|
||||||
|
|
||||||
|
delete params[CSI_NODE_EXPAND_SECRET_NAME];
|
||||||
|
delete params[CSI_NODE_EXPAND_SECRET_NAMESPACE];
|
||||||
|
this.value['parameters'] = params;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
@ -337,6 +383,16 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="enableOnlineExpansionVolumeEncryption"
|
||||||
|
class="col span-6 flex items-center mt-20"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
v-model:value="volumeExpansionCheckBoxEnabled"
|
||||||
|
:label="t('harvester.storage.volumeExpansionCheckbox')"
|
||||||
|
:disabled="isEdit || isView"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<KeyValue
|
<KeyValue
|
||||||
|
|||||||
@ -254,15 +254,24 @@ export default {
|
|||||||
v-if="restoreNewVm"
|
v-if="restoreNewVm"
|
||||||
v-model:value="restoreResource.spec.keepMacAddress"
|
v-model:value="restoreResource.spec.keepMacAddress"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
class="check mb-20"
|
||||||
:label="t('harvester.backup.restore.keepMacAddress')"
|
:label="t('harvester.backup.restore.keepMacAddress')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-if="!restoreNewVm"
|
v-if="!restoreNewVm"
|
||||||
v-model:value="deletionPolicy"
|
v-model:value="deletionPolicy"
|
||||||
|
class="mb-20"
|
||||||
:label="t('harvester.backup.restore.deletePreviousVolumes')"
|
:label="t('harvester.backup.restore.deletePreviousVolumes')"
|
||||||
:options="deletionPolicyOption"
|
:options="deletionPolicyOption"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
v-model:value="restoreResource.spec.haltAfterRestore"
|
||||||
|
type="checkbox"
|
||||||
|
class="check mb-20"
|
||||||
|
:label="t('harvester.backup.restore.haltAfterRestore')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer
|
<Footer
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import CpuMemory from './kubevirt.io.virtualmachine/VirtualMachineCpuMemory';
|
|||||||
import CpuModel from './kubevirt.io.virtualmachine/VirtualMachineCpuModel';
|
import CpuModel from './kubevirt.io.virtualmachine/VirtualMachineCpuModel';
|
||||||
import CloudConfig from './kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
import CloudConfig from './kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
||||||
import SSHKey from './kubevirt.io.virtualmachine/VirtualMachineSSHKey';
|
import SSHKey from './kubevirt.io.virtualmachine/VirtualMachineSSHKey';
|
||||||
|
import Filesystem from './kubevirt.io.virtualmachine/VirtualMachineFilesystem';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditVMTemplate',
|
name: 'HarvesterEditVMTemplate',
|
||||||
@ -51,6 +52,7 @@ export default {
|
|||||||
UnitInput,
|
UnitInput,
|
||||||
Banner,
|
Banner,
|
||||||
KeyValue,
|
KeyValue,
|
||||||
|
Filesystem,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView, VM_MIXIN],
|
mixins: [CreateEditView, VM_MIXIN],
|
||||||
@ -95,6 +97,10 @@ export default {
|
|||||||
secretNamePrefix() {
|
secretNamePrefix() {
|
||||||
return this.templateValue?.metadata?.name;
|
return this.templateValue?.metadata?.name;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
filesystemEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('supportFilesystem');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@ -154,6 +160,7 @@ export default {
|
|||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.imageId = this.diskRows[0]?.image || '';
|
this.imageId = this.diskRows[0]?.image || '';
|
||||||
|
this['filesystemRows'] = this.getFilesystemRows(this.value.spec.vm);
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@ -349,6 +356,19 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
v-if="filesystemEnabled"
|
||||||
|
name="filesystem"
|
||||||
|
:label="t('harvester.tab.filesystem')"
|
||||||
|
:weight="-8"
|
||||||
|
>
|
||||||
|
<Filesystem
|
||||||
|
v-model:value="filesystemRows"
|
||||||
|
:mode="mode"
|
||||||
|
:namespace="templateValue.metadata.namespace"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
name="labels"
|
name="labels"
|
||||||
:label="t('generic.labels')"
|
:label="t('generic.labels')"
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import Footer from '@shell/components/form/Footer';
|
import Footer from '@shell/components/form/Footer';
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import Checkbox from '@components/Form/Checkbox/Checkbox';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
@ -32,6 +33,7 @@ const createObject = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'CreateRestore',
|
name: 'CreateRestore',
|
||||||
components: {
|
components: {
|
||||||
|
Checkbox,
|
||||||
Footer,
|
Footer,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
LabeledInput,
|
LabeledInput,
|
||||||
@ -249,9 +251,17 @@ export default {
|
|||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-if="!restoreNewVm"
|
v-if="!restoreNewVm"
|
||||||
v-model:value="deletionPolicy"
|
v-model:value="deletionPolicy"
|
||||||
|
class="mb-20"
|
||||||
:label="t('harvester.backup.restore.deletePreviousVolumes')"
|
:label="t('harvester.backup.restore.deletePreviousVolumes')"
|
||||||
:options="deletionPolicyOption"
|
:options="deletionPolicyOption"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
v-model:value="restoreResource.spec.haltAfterRestore"
|
||||||
|
type="checkbox"
|
||||||
|
class="check mb-20"
|
||||||
|
:label="t('harvester.backup.restore.haltAfterRestore')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer
|
<Footer
|
||||||
|
|||||||
190
pkg/harvester/edit/kubeovn.io.iptablesdnatrule.vue
Normal file
190
pkg/harvester/edit/kubeovn.io.iptablesdnatrule.vue
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
<script>
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
CruResource,
|
||||||
|
NameNsDescription,
|
||||||
|
ResourceTabs,
|
||||||
|
Tab,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
eip: this.value?.spec?.eip || '',
|
||||||
|
externalPort: this.value?.spec?.externalPort || '',
|
||||||
|
internalIp: this.value?.spec?.internalIp || '',
|
||||||
|
internalPort: this.value?.spec?.internalPort || '',
|
||||||
|
protocol: this.value?.spec?.protocol || '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
await allHash({ eips: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.IPTABLES_EIP }) });
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.updateBeforeSave);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
eipOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const eips = this.$store.getters[`${ inStore }/all`](HCI.IPTABLES_EIP) || [];
|
||||||
|
|
||||||
|
return eips.map((eip) => ({
|
||||||
|
label: eip.id,
|
||||||
|
value: eip.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
protocolOptions() {
|
||||||
|
return [
|
||||||
|
{ label: 'TCP', value: 'tcp' },
|
||||||
|
{ label: 'UDP', value: 'udp' },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updateBeforeSave() {
|
||||||
|
if (!this.value.spec) {
|
||||||
|
this.value.spec = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value.spec.eip = this.eip;
|
||||||
|
this.value.spec.externalPort = this.externalPort;
|
||||||
|
this.value.spec.internalIp = this.internalIp;
|
||||||
|
this.value.spec.internalPort = this.internalPort;
|
||||||
|
this.value.spec.protocol = this.protocol;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:errors="errors"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
@finish="save"
|
||||||
|
@error="e=>errors=e"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
ref="nd"
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:namespaced="false"
|
||||||
|
@update:value="$emit('update:value', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ResourceTabs
|
||||||
|
class="mt-15"
|
||||||
|
:need-conditions="false"
|
||||||
|
:need-related="false"
|
||||||
|
:need-events="false"
|
||||||
|
:side-tabs="true"
|
||||||
|
:mode="mode"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="basic"
|
||||||
|
:label="t('generic.basic')"
|
||||||
|
:weight="99"
|
||||||
|
>
|
||||||
|
<div class="mt-20">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="eip"
|
||||||
|
class="mb-20"
|
||||||
|
:options="eipOptions"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.dnat.eip.label')"
|
||||||
|
:placeholder="t('harvester.dnat.eip.placeholder')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="externalPort"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.dnat.externalPort.label')"
|
||||||
|
:placeholder="t('harvester.dnat.externalPort.placeholder')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="internalIp"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.dnat.internalIp.label')"
|
||||||
|
:placeholder="t('harvester.dnat.internalIp.placeholder')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="internalPort"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.dnat.internalPort.label')"
|
||||||
|
:placeholder="t('harvester.dnat.internalPort.placeholder')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="protocol"
|
||||||
|
class="mb-20"
|
||||||
|
:options="protocolOptions"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.dnat.protocol.label')"
|
||||||
|
:placeholder="t('harvester.dnat.protocol.placeholder')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</ResourceTabs>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
168
pkg/harvester/edit/kubeovn.io.iptableseip.vue
Normal file
168
pkg/harvester/edit/kubeovn.io.iptableseip.vue
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
<script>
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
CruResource,
|
||||||
|
NameNsDescription,
|
||||||
|
ResourceTabs,
|
||||||
|
Tab,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
natGwDp: this.value?.spec?.natGwDp || '',
|
||||||
|
externalSubnet: this.value?.spec?.externalSubnet || '',
|
||||||
|
v4ip: this.value?.spec?.v4ip || '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
await allHash({
|
||||||
|
natGateways: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC_NAT_GATEWAY }),
|
||||||
|
subnets: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SUBNET }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.updateBeforeSave);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
natGatewayOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const natGateways = this.$store.getters[`${ inStore }/all`](HCI.VPC_NAT_GATEWAY) || [];
|
||||||
|
|
||||||
|
return natGateways.map((gw) => ({
|
||||||
|
label: gw.id,
|
||||||
|
value: gw.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
subnetOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const subnets = this.$store.getters[`${ inStore }/all`](HCI.SUBNET) || [];
|
||||||
|
|
||||||
|
return subnets.map((subnet) => ({
|
||||||
|
label: subnet.id,
|
||||||
|
value: subnet.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updateBeforeSave() {
|
||||||
|
if (!this.value.spec) {
|
||||||
|
this.value.spec = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value.spec.natGwDp = this.natGwDp;
|
||||||
|
this.value.spec.externalSubnet = this.externalSubnet;
|
||||||
|
this.value.spec.v4ip = this.v4ip;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:errors="errors"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
@finish="save"
|
||||||
|
@error="e=>errors=e"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
ref="nd"
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:namespaced="false"
|
||||||
|
@update:value="$emit('update:value', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ResourceTabs
|
||||||
|
class="mt-15"
|
||||||
|
:need-conditions="false"
|
||||||
|
:need-related="false"
|
||||||
|
:need-events="false"
|
||||||
|
:side-tabs="true"
|
||||||
|
:mode="mode"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="basic"
|
||||||
|
:label="t('generic.basic')"
|
||||||
|
:weight="99"
|
||||||
|
>
|
||||||
|
<div class="mt-20">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="natGwDp"
|
||||||
|
class="mb-20"
|
||||||
|
:options="natGatewayOptions"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.externalIP.natGateway.label')"
|
||||||
|
:placeholder="t('harvester.externalIP.natGateway.placeholder')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="externalSubnet"
|
||||||
|
class="mb-20"
|
||||||
|
:options="subnetOptions"
|
||||||
|
:label="t('harvester.externalIP.externalSubnet.label')"
|
||||||
|
:placeholder="t('harvester.externalIP.externalSubnet.placeholder')"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="v4ip"
|
||||||
|
class="mb-20"
|
||||||
|
:label="t('harvester.externalIP.v4ip.label')"
|
||||||
|
:placeholder="t('harvester.externalIP.v4ip.placeholder')"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</ResourceTabs>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
140
pkg/harvester/edit/kubeovn.io.iptablessnatrule.vue
Normal file
140
pkg/harvester/edit/kubeovn.io.iptablessnatrule.vue
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<script>
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
CruResource,
|
||||||
|
NameNsDescription,
|
||||||
|
ResourceTabs,
|
||||||
|
Tab,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
eip: this.value?.spec?.eip || '',
|
||||||
|
internalCIDR: this.value?.spec?.internalCIDR || '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
await allHash({ eips: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.IPTABLES_EIP }) });
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.updateBeforeSave);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
eipOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const eips = this.$store.getters[`${ inStore }/all`](HCI.IPTABLES_EIP) || [];
|
||||||
|
|
||||||
|
return eips.map((eip) => ({
|
||||||
|
label: eip.id,
|
||||||
|
value: eip.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updateBeforeSave() {
|
||||||
|
if (!this.value.spec) {
|
||||||
|
this.value.spec = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value.spec.eip = this.eip;
|
||||||
|
this.value.spec.internalCIDR = this.internalCIDR;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:errors="errors"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
@finish="save"
|
||||||
|
@error="e=>errors=e"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
ref="nd"
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:namespaced="false"
|
||||||
|
@update:value="$emit('update:value', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ResourceTabs
|
||||||
|
class="mt-15"
|
||||||
|
:need-conditions="false"
|
||||||
|
:need-related="false"
|
||||||
|
:need-events="false"
|
||||||
|
:side-tabs="true"
|
||||||
|
:mode="mode"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="basic"
|
||||||
|
:label="t('generic.basic')"
|
||||||
|
:weight="99"
|
||||||
|
>
|
||||||
|
<div class="mt-20">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="eip"
|
||||||
|
class="mb-20"
|
||||||
|
:options="eipOptions"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.snat.eip.label')"
|
||||||
|
:placeholder="t('harvester.snat.eip.placeholder')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="internalCIDR"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.snat.internalCIDR.label')"
|
||||||
|
:placeholder="t('harvester.snat.internalCIDR.placeholder')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</ResourceTabs>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
384
pkg/harvester/edit/kubeovn.io.providernetwork.vue
Normal file
384
pkg/harvester/edit/kubeovn.io.providernetwork.vue
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
<script>
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import ArrayList from '@shell/components/form/ArrayList';
|
||||||
|
import InfoBox from '@shell/components/InfoBox';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import ArrayListSelect from '@shell/components/form/ArrayListSelect';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { NODE } from '@shell/config/types';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
CruResource,
|
||||||
|
ArrayList,
|
||||||
|
InfoBox,
|
||||||
|
NameNsDescription,
|
||||||
|
ResourceTabs,
|
||||||
|
Tab,
|
||||||
|
LabeledSelect,
|
||||||
|
ArrayListSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
defaultInterface: this.value?.spec?.defaultInterface || '',
|
||||||
|
excludedNodes: this.value?.spec?.excludeNodes || [],
|
||||||
|
customInterfaces: this.value?.spec?.customInterfaces || [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
await allHash({
|
||||||
|
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
|
||||||
|
linkMonitors: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.LINK_MONITOR }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.updateBeforeSave);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
nodes() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const nodes = this.$store.getters[`${ inStore }/all`](NODE);
|
||||||
|
|
||||||
|
return nodes.filter((n) => n.isEtcd !== 'true');
|
||||||
|
},
|
||||||
|
|
||||||
|
nics() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const linkMonitor = this.$store.getters[`${ inStore }/byId`](HCI.LINK_MONITOR, 'nic') || {};
|
||||||
|
const linkStatus = linkMonitor?.status?.linkStatus || {};
|
||||||
|
const nodes = this.nodes.map((n) => n.id);
|
||||||
|
|
||||||
|
const out = [];
|
||||||
|
|
||||||
|
// Collect all nics from all nodes
|
||||||
|
Object.keys(linkStatus).map((nodeName) => {
|
||||||
|
if (nodes.includes(nodeName)) {
|
||||||
|
const nics = linkStatus[nodeName] || [];
|
||||||
|
|
||||||
|
nics.map((nic) => {
|
||||||
|
out.push({
|
||||||
|
...nic,
|
||||||
|
nodeName,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
nicOptions() {
|
||||||
|
const out = [];
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
(this.nics || []).forEach((nic) => {
|
||||||
|
if (!seen.has(nic.name)) {
|
||||||
|
seen.add(nic.name);
|
||||||
|
out.push({
|
||||||
|
label: nic.name,
|
||||||
|
value: nic.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return out.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
},
|
||||||
|
|
||||||
|
nodeOptions() {
|
||||||
|
return this.nodes.map((node) => ({
|
||||||
|
label: node.id,
|
||||||
|
value: node.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
removeCustomInterface(index) {
|
||||||
|
this.customInterfaces.splice(index, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateBeforeSave() {
|
||||||
|
if (!this.value.spec) {
|
||||||
|
this.value.spec = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value.spec.defaultInterface = this.defaultInterface;
|
||||||
|
this.value.spec.excludeNodes = this.excludedNodes;
|
||||||
|
this.value.spec.customInterfaces = (this.customInterfaces || [])
|
||||||
|
.filter((item) => item?.interface || (item?.nodes || []).length)
|
||||||
|
.map((item) => ({
|
||||||
|
interface: item.interface || '',
|
||||||
|
nodes: (item.nodes || []).filter((node) => !!node),
|
||||||
|
}))
|
||||||
|
.filter((item) => item.interface && item.nodes.length > 0);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:errors="errors"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
@finish="save"
|
||||||
|
@error="e=>errors=e"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
ref="nd"
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:namespaced="false"
|
||||||
|
@update:value="$emit('update:value', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ResourceTabs
|
||||||
|
class="mt-15"
|
||||||
|
:need-conditions="false"
|
||||||
|
:need-related="false"
|
||||||
|
:need-events="false"
|
||||||
|
:side-tabs="true"
|
||||||
|
:mode="mode"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="Interfaces"
|
||||||
|
label="Interfaces"
|
||||||
|
:weight="99"
|
||||||
|
>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="defaultInterface"
|
||||||
|
class="mb-20"
|
||||||
|
required
|
||||||
|
:options="nicOptions"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.providerNetwork.defaultInterface.label')"
|
||||||
|
:placeholder="t('harvester.providerNetwork.defaultInterface.placeholder')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<hr class="section-divider" />
|
||||||
|
|
||||||
|
<ArrayList
|
||||||
|
v-model:value="customInterfaces"
|
||||||
|
class="mb-20 custom-interface-list"
|
||||||
|
:mode="mode"
|
||||||
|
:title="t('harvester.providerNetwork.customInterfaces.label')"
|
||||||
|
:protip="false"
|
||||||
|
:remove-allowed="false"
|
||||||
|
:initial-empty-row="true"
|
||||||
|
:default-add-value="{ interface: '', nodes: [] }"
|
||||||
|
>
|
||||||
|
<template #add="{ add }">
|
||||||
|
<div class="custom-interface-primary-add">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn role-primary"
|
||||||
|
:disabled="mode === 'view'"
|
||||||
|
@click="add"
|
||||||
|
>
|
||||||
|
{{ t('harvester.providerNetwork.customInterfaces.addLabel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #column-headers>
|
||||||
|
<div class="row custom-interface-header">
|
||||||
|
<div class="col span-6">
|
||||||
|
{{ t('harvester.providerNetwork.customInterfaces.interface.label') }}
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
{{ t('harvester.providerNetwork.customInterfaces.nodes.label') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #columns="scope">
|
||||||
|
<InfoBox class="custom-interface-box">
|
||||||
|
<button
|
||||||
|
v-if="mode !== 'view'"
|
||||||
|
type="button"
|
||||||
|
class="role-link btn btn-sm remove"
|
||||||
|
@click="removeCustomInterface(scope.i)"
|
||||||
|
>
|
||||||
|
<i class="icon icon-x" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="custom-interface-content">
|
||||||
|
<div class="row custom-interface-row interface-row">
|
||||||
|
<div class="col span-12 interface-col">
|
||||||
|
<h3 class="mb-10">
|
||||||
|
{{ t('harvester.providerNetwork.customInterfaces.interface.label') }}
|
||||||
|
</h3>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="scope.row.value.interface"
|
||||||
|
class="mb-20"
|
||||||
|
:label="''"
|
||||||
|
:options="nicOptions"
|
||||||
|
:mode="mode"
|
||||||
|
:placeholder="t('harvester.providerNetwork.customInterfaces.interface.placeholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row custom-interface-row nodes-row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<ArrayListSelect
|
||||||
|
v-model:value="scope.row.value.nodes"
|
||||||
|
:options="nodeOptions"
|
||||||
|
:mode="mode"
|
||||||
|
:disabled="mode === 'view'"
|
||||||
|
:enable-default-add-value="false"
|
||||||
|
:array-list-props="{
|
||||||
|
addLabel: t('harvester.providerNetwork.customInterfaces.nodes.addLabel'),
|
||||||
|
initialEmptyRow: true,
|
||||||
|
title: t('harvester.providerNetwork.customInterfaces.nodes.label'),
|
||||||
|
required: false,
|
||||||
|
protip: false,
|
||||||
|
}"
|
||||||
|
:select-props="{
|
||||||
|
placeholder: t('harvester.providerNetwork.customInterfaces.nodes.placeholder'),
|
||||||
|
disabled: mode === 'view',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #add="{ add }">
|
||||||
|
<div class="custom-interface-add">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn role-tertiary add"
|
||||||
|
:disabled="mode === 'view'"
|
||||||
|
@click="add"
|
||||||
|
>
|
||||||
|
{{ t('harvester.providerNetwork.customInterfaces.nodes.addLabel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ArrayListSelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</InfoBox>
|
||||||
|
</template>
|
||||||
|
</ArrayList>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
name="excludedNodes"
|
||||||
|
:label="t('harvester.providerNetwork.excludedNodes.label')"
|
||||||
|
:weight="98"
|
||||||
|
>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<ArrayListSelect
|
||||||
|
v-model:value="excludedNodes"
|
||||||
|
:options="nodeOptions"
|
||||||
|
:disabled="mode === 'view'"
|
||||||
|
:mode="mode"
|
||||||
|
:enable-default-add-value="false"
|
||||||
|
:array-list-props="{
|
||||||
|
addLabel: t('harvester.providerNetwork.excludedNodes.addLabel'),
|
||||||
|
initialEmptyRow: true,
|
||||||
|
required: false,
|
||||||
|
protip: false,
|
||||||
|
}"
|
||||||
|
:select-props="{
|
||||||
|
placeholder: t('harvester.providerNetwork.excludedNodes.placeholder'),
|
||||||
|
disabled: mode === 'view',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</ResourceTabs>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.section-divider {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
margin: 10px 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-interface-header {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-interface-row {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interface-row {
|
||||||
|
width: calc(100% - 90px);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodes-row {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-interface-add {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.nodes-row .array-list-select .box) {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.nodes-row .array-list-select .box .remove) {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-interface-primary-add {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-interface-box {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.custom-interface-list .box) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -43,6 +43,7 @@ export default {
|
|||||||
created() {
|
created() {
|
||||||
const vpc = this.$route.query.vpc || '';
|
const vpc = this.$route.query.vpc || '';
|
||||||
const enableDHCP = this.value?.spec?.enableDHCP || false;
|
const enableDHCP = this.value?.spec?.enableDHCP || false;
|
||||||
|
const natOutgoing = this.value?.spec?.natOutgoing || false;
|
||||||
|
|
||||||
set(this.value.spec, 'enableDHCP', enableDHCP);
|
set(this.value.spec, 'enableDHCP', enableDHCP);
|
||||||
set(this.value, 'spec', this.value.spec || {
|
set(this.value, 'spec', this.value.spec || {
|
||||||
@ -50,10 +51,11 @@ export default {
|
|||||||
protocol: NETWORK_PROTOCOL.IPv4,
|
protocol: NETWORK_PROTOCOL.IPv4,
|
||||||
provider: '',
|
provider: '',
|
||||||
vpc,
|
vpc,
|
||||||
gatewayIP: '',
|
gateway: '',
|
||||||
excludeIps: [],
|
excludeIps: [],
|
||||||
private: false,
|
private: false,
|
||||||
enableDHCP,
|
enableDHCP,
|
||||||
|
natOutgoing,
|
||||||
acls: []
|
acls: []
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -64,6 +66,7 @@ export default {
|
|||||||
const hash = {
|
const hash = {
|
||||||
vpc: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
|
vpc: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
|
||||||
nad: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }),
|
nad: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }),
|
||||||
|
vlans: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN }),
|
||||||
};
|
};
|
||||||
|
|
||||||
await allHash(hash);
|
await allHash(hash);
|
||||||
@ -129,6 +132,20 @@ export default {
|
|||||||
label: n.id,
|
label: n.id,
|
||||||
value: n.id,
|
value: n.id,
|
||||||
}));
|
}));
|
||||||
|
},
|
||||||
|
natOutgoingDisabled() {
|
||||||
|
// Disable the NAT Outgoing option when the subnet belongs to the ovn-cluster VPC and its name is join or ovn-default.
|
||||||
|
return this.value?.spec?.vpc === 'ovn-cluster' && ['join', 'ovn-default'].includes(this.value?.metadata?.name);
|
||||||
|
},
|
||||||
|
|
||||||
|
vlanOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const vlans = this.$store.getters[`${ inStore }/all`](HCI.VLAN) || [];
|
||||||
|
|
||||||
|
return vlans.map((vlan) => ({
|
||||||
|
label: vlan.id,
|
||||||
|
value: vlan.id,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -264,6 +281,16 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="value.spec.vlan"
|
||||||
|
class="mb-20"
|
||||||
|
:options="vlanOptions"
|
||||||
|
:placeholder="t('harvester.subnet.vlan.placeholder')"
|
||||||
|
:label="t('harvester.subnet.vlan.label')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-20">
|
<div class="row mt-20">
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
@ -304,6 +331,20 @@ export default {
|
|||||||
</Banner>
|
</Banner>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="value.spec.natOutgoing"
|
||||||
|
name="enableExternalConnectivity"
|
||||||
|
:disabled="natOutgoingDisabled"
|
||||||
|
:options="[true, false]"
|
||||||
|
:label="t('harvester.subnet.externalConnectivity.label')"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
:mode="mode"
|
||||||
|
:tooltip="t('harvester.subnet.externalConnectivity.tooltip')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row mt-20">
|
<div class="row mt-20">
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
|
|||||||
146
pkg/harvester/edit/kubeovn.io.vlan.vue
Normal file
146
pkg/harvester/edit/kubeovn.io.vlan.vue
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
<script>
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
CruResource,
|
||||||
|
NameNsDescription,
|
||||||
|
ResourceTabs,
|
||||||
|
Tab,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
vlanId: this.value?.spec?.id || '',
|
||||||
|
provider: this.value?.spec?.provider || '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
await allHash({ providerNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.PROVIDER_NETWORK }) });
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.updateBeforeSave);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
providerNetworks() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const providerNetworks = this.$store.getters[`${ inStore }/all`](HCI.PROVIDER_NETWORK) || [];
|
||||||
|
|
||||||
|
return providerNetworks.map((pn) => ({
|
||||||
|
label: pn.id,
|
||||||
|
value: pn.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updateBeforeSave() {
|
||||||
|
if (!this.value.spec) {
|
||||||
|
this.value.spec = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.vlanId !== '') {
|
||||||
|
this.value.spec.id = Number(this.vlanId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value.spec.provider = this.provider;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:errors="errors"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
@finish="save"
|
||||||
|
@error="e=>errors=e"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
ref="nd"
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:namespaced="false"
|
||||||
|
@update:value="$emit('update:value', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ResourceTabs
|
||||||
|
class="mt-15"
|
||||||
|
:need-conditions="false"
|
||||||
|
:need-related="false"
|
||||||
|
:need-events="false"
|
||||||
|
:side-tabs="true"
|
||||||
|
:mode="mode"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="basic"
|
||||||
|
:label="t('generic.basic')"
|
||||||
|
:weight="99"
|
||||||
|
>
|
||||||
|
<div class="mt-20">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value.number="vlanId"
|
||||||
|
class="mb-20"
|
||||||
|
type="number"
|
||||||
|
:min="1"
|
||||||
|
:max="4094"
|
||||||
|
:label="t('harvester.vlan.id.label')"
|
||||||
|
:placeholder="t('harvester.vlan.id.placeholder')"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="provider"
|
||||||
|
class="mb-20"
|
||||||
|
:options="providerNetworks"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.vlan.provider.label')"
|
||||||
|
:placeholder="t('harvester.vlan.provider.placeholder')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</ResourceTabs>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
246
pkg/harvester/edit/kubeovn.io.vpcnatgateway.vue
Normal file
246
pkg/harvester/edit/kubeovn.io.vpcnatgateway.vue
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
<script>
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import ArrayListSelect from '@shell/components/form/ArrayListSelect';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/config/labels-annotations';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
CruResource,
|
||||||
|
NameNsDescription,
|
||||||
|
ResourceTabs,
|
||||||
|
Tab,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
ArrayListSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const internalTenantNetwork = this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.CNI_NETWORKS] || '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
internalTenantNetwork,
|
||||||
|
vpc: this.value?.spec?.vpc || '',
|
||||||
|
subnet: this.value?.spec?.subnet || '',
|
||||||
|
lanIp: this.value?.spec?.lanIp || '',
|
||||||
|
externalSubnets: this.value?.spec?.externalSubnets || [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
await allHash({
|
||||||
|
vpcs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
|
||||||
|
subnets: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SUBNET }),
|
||||||
|
vmNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.NETWORK_ATTACHMENT }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.updateBeforeSave);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
vpcOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const vpcs = this.$store.getters[`${ inStore }/all`](HCI.VPC) || [];
|
||||||
|
|
||||||
|
return vpcs.map((vpc) => ({
|
||||||
|
label: vpc.id,
|
||||||
|
value: vpc.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
subnetOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const subnets = this.$store.getters[`${ inStore }/all`](HCI.SUBNET) || [];
|
||||||
|
|
||||||
|
return subnets.map((subnet) => ({
|
||||||
|
label: subnet.id,
|
||||||
|
value: subnet.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
vmNetworkOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const vmNetworks = this.$store.getters[`${ inStore }/all`](HCI.NETWORK_ATTACHMENT) || [];
|
||||||
|
|
||||||
|
return vmNetworks.map((network) => ({
|
||||||
|
label: network.id,
|
||||||
|
value: network.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updateBeforeSave() {
|
||||||
|
if (!this.value.spec) {
|
||||||
|
this.value.spec = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.value.metadata) {
|
||||||
|
this.value.metadata = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.value.metadata.annotations) {
|
||||||
|
this.value.metadata.annotations = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value.spec.vpc = this.vpc;
|
||||||
|
this.value.spec.subnet = this.subnet;
|
||||||
|
this.value.spec.lanIp = this.lanIp;
|
||||||
|
this.value.spec.externalSubnets = (this.externalSubnets || []).filter((subnet) => !!subnet);
|
||||||
|
|
||||||
|
if (this.internalTenantNetwork) {
|
||||||
|
this.value.metadata.annotations[HCI_ANNOTATIONS.CNI_NETWORKS] = this.internalTenantNetwork;
|
||||||
|
} else {
|
||||||
|
delete this.value.metadata.annotations[HCI_ANNOTATIONS.CNI_NETWORKS];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:errors="errors"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
@finish="save"
|
||||||
|
@error="e=>errors=e"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
ref="nd"
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:namespaced="false"
|
||||||
|
@update:value="$emit('update:value', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ResourceTabs
|
||||||
|
class="mt-15"
|
||||||
|
:need-conditions="false"
|
||||||
|
:need-related="false"
|
||||||
|
:need-events="false"
|
||||||
|
:side-tabs="true"
|
||||||
|
:mode="mode"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="basic"
|
||||||
|
:label="t('generic.basic')"
|
||||||
|
:weight="99"
|
||||||
|
>
|
||||||
|
<div class="mt-20">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="internalTenantNetwork"
|
||||||
|
class="mb-20"
|
||||||
|
required
|
||||||
|
:options="vmNetworkOptions"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.natGateway.internalTenantNetwork.label')"
|
||||||
|
:placeholder="t('harvester.natGateway.internalTenantNetwork.placeholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="vpc"
|
||||||
|
class="mb-20"
|
||||||
|
:options="vpcOptions"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.natGateway.vpc.label')"
|
||||||
|
:placeholder="t('harvester.natGateway.vpc.placeholder')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="subnet"
|
||||||
|
class="mb-20"
|
||||||
|
:options="subnetOptions"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.natGateway.subnet.label')"
|
||||||
|
:placeholder="t('harvester.natGateway.subnet.placeholder')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="lanIp"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.natGateway.lanIp.label')"
|
||||||
|
:placeholder="t('harvester.natGateway.lanIp.placeholder')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
name="externalSubnets"
|
||||||
|
:label="t('harvester.natGateway.externalSubnets.label')"
|
||||||
|
:weight="98"
|
||||||
|
>
|
||||||
|
<div class="mt-20">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<ArrayListSelect
|
||||||
|
v-model:value="externalSubnets"
|
||||||
|
:mode="mode"
|
||||||
|
:disabled="mode === 'view'"
|
||||||
|
required
|
||||||
|
:options="subnetOptions"
|
||||||
|
:enable-default-add-value="false"
|
||||||
|
:array-list-props="{
|
||||||
|
addLabel: t('harvester.natGateway.externalSubnets.addLabel'),
|
||||||
|
title: t('harvester.natGateway.subnet.label'),
|
||||||
|
initialEmptyRow: true,
|
||||||
|
required: true,
|
||||||
|
protip: false,
|
||||||
|
}"
|
||||||
|
:select-props="{
|
||||||
|
placeholder: t('harvester.natGateway.externalSubnets.placeholder'),
|
||||||
|
disabled: mode === 'view',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</ResourceTabs>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
@ -143,6 +143,7 @@ export default {
|
|||||||
<YamlEditor
|
<YamlEditor
|
||||||
ref="yaml"
|
ref="yaml"
|
||||||
v-model:value="yamlScript"
|
v-model:value="yamlScript"
|
||||||
|
:mode="mode"
|
||||||
class="yaml-editor"
|
class="yaml-editor"
|
||||||
:editor-mode="editorMode"
|
:editor-mode="editorMode"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -0,0 +1,310 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { CONFIG_MAP, SECRET, SERVICE_ACCOUNT } from '@shell/config/types';
|
||||||
|
import { _VIEW } from '@shell/config/query-params';
|
||||||
|
import CopyToClipboard from '@shell/components/CopyToClipboard';
|
||||||
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
|
import { FILESYSTEM_SOURCE_TYPE } from '@pkg/harvester/config/types';
|
||||||
|
|
||||||
|
const MAX_FILESYSTEMS = 3;
|
||||||
|
|
||||||
|
const { CONFIGMAP: FS_TYPE_CONFIGMAP, SECRET: FS_TYPE_SECRET, SERVICEACCOUNT: FS_TYPE_SERVICEACCOUNT } = FILESYSTEM_SOURCE_TYPE;
|
||||||
|
|
||||||
|
const DEFAULT_VOLUME_NAMES = {
|
||||||
|
[FS_TYPE_CONFIGMAP]: 'appconfigfs',
|
||||||
|
[FS_TYPE_SECRET]: 'appsecretfs',
|
||||||
|
[FS_TYPE_SERVICEACCOUNT]: 'appserviceaccountfs',
|
||||||
|
};
|
||||||
|
|
||||||
|
const FS_TYPE_OPTIONS = [
|
||||||
|
{ label: 'ConfigMap', value: FS_TYPE_CONFIGMAP },
|
||||||
|
{ label: 'Secret', value: FS_TYPE_SECRET },
|
||||||
|
{ label: 'ServiceAccount', value: FS_TYPE_SERVICEACCOUNT },
|
||||||
|
];
|
||||||
|
|
||||||
|
function emptyRow() {
|
||||||
|
return {
|
||||||
|
fsType: FS_TYPE_CONFIGMAP,
|
||||||
|
volumeName: DEFAULT_VOLUME_NAMES[FS_TYPE_CONFIGMAP],
|
||||||
|
resourceName: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VirtualMachineFilesystem',
|
||||||
|
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Banner,
|
||||||
|
LabeledSelect,
|
||||||
|
LabeledInput,
|
||||||
|
CopyToClipboard,
|
||||||
|
MessageLink,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: 'create',
|
||||||
|
},
|
||||||
|
namespace: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { rows: this.value.length > 0 ? this.value.map((r) => ({ ...r })) : [emptyRow()] };
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
const incoming = JSON.stringify(newVal);
|
||||||
|
const current = JSON.stringify(this.rows);
|
||||||
|
|
||||||
|
if (incoming !== current) {
|
||||||
|
this.rows = newVal.map((r) => ({ ...r }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
rows: {
|
||||||
|
deep: true,
|
||||||
|
handler(val) {
|
||||||
|
this.$emit('update:value', val.map((r) => ({ ...r })));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
inStore() {
|
||||||
|
return this.$store.getters['currentProduct'].inStore;
|
||||||
|
},
|
||||||
|
|
||||||
|
configMaps() {
|
||||||
|
return this.$store.getters[`${ this.inStore }/all`](CONFIG_MAP)
|
||||||
|
.filter((cm) => !this.namespace || cm.metadata.namespace === this.namespace)
|
||||||
|
.map((cm) => ({ label: cm.metadata.name, value: cm.metadata.name }));
|
||||||
|
},
|
||||||
|
|
||||||
|
secrets() {
|
||||||
|
return this.$store.getters[`${ this.inStore }/all`](SECRET)
|
||||||
|
.filter((s) => !this.namespace || s.metadata.namespace === this.namespace)
|
||||||
|
.map((s) => ({ label: s.metadata.name, value: s.metadata.name }));
|
||||||
|
},
|
||||||
|
|
||||||
|
serviceAccounts() {
|
||||||
|
return this.$store.getters[`${ this.inStore }/all`](SERVICE_ACCOUNT)
|
||||||
|
.filter((sa) => !this.namespace || sa.metadata.namespace === this.namespace)
|
||||||
|
.map((sa) => ({ label: sa.metadata.name, value: sa.metadata.name }));
|
||||||
|
},
|
||||||
|
|
||||||
|
canAddRow() {
|
||||||
|
return this.rows.length < MAX_FILESYSTEMS;
|
||||||
|
},
|
||||||
|
|
||||||
|
isView() {
|
||||||
|
return this.mode === _VIEW;
|
||||||
|
},
|
||||||
|
|
||||||
|
completedRows() {
|
||||||
|
return this.rows.filter((r) => r.fsType && r.volumeName && r.resourceName);
|
||||||
|
},
|
||||||
|
|
||||||
|
allMountCommands() {
|
||||||
|
return this.completedRows.map((r) => this.mountCommands(r)).join('\n');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
fsTypeOptions(currentIndex) {
|
||||||
|
const usedTypes = this.rows
|
||||||
|
.filter((_, i) => i !== currentIndex)
|
||||||
|
.map((r) => r.fsType);
|
||||||
|
|
||||||
|
return FS_TYPE_OPTIONS.filter((opt) => !usedTypes.includes(opt.value));
|
||||||
|
},
|
||||||
|
|
||||||
|
resourceOptions(fsType) {
|
||||||
|
if (fsType === FS_TYPE_CONFIGMAP) return this.configMaps;
|
||||||
|
if (fsType === FS_TYPE_SECRET) return this.secrets;
|
||||||
|
if (fsType === FS_TYPE_SERVICEACCOUNT) return this.serviceAccounts;
|
||||||
|
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
|
||||||
|
onFsTypeChange(row, newType) {
|
||||||
|
row.fsType = newType;
|
||||||
|
row.volumeName = DEFAULT_VOLUME_NAMES[newType] || '';
|
||||||
|
row.resourceName = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
addRow() {
|
||||||
|
if (this.canAddRow) {
|
||||||
|
const usedTypes = this.rows.map((r) => r.fsType);
|
||||||
|
const nextType = FS_TYPE_OPTIONS.find((opt) => !usedTypes.includes(opt.value))?.value || FS_TYPE_CONFIGMAP;
|
||||||
|
|
||||||
|
this.rows.push({
|
||||||
|
fsType: nextType,
|
||||||
|
volumeName: DEFAULT_VOLUME_NAMES[nextType] || '',
|
||||||
|
resourceName: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeRow(index) {
|
||||||
|
this.rows.splice(index, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
mountCommands(row) {
|
||||||
|
const vol = row.volumeName || '<volume-name>';
|
||||||
|
|
||||||
|
return `- mkdir -p /mnt/${ vol }\n- mount -t virtiofs ${ vol } /mnt/${ vol }`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="vm-filesystem">
|
||||||
|
<p class="mb-20">
|
||||||
|
{{ t('harvester.virtualMachine.filesystem.description') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="(row, index) in rows"
|
||||||
|
:key="index"
|
||||||
|
class="filesystem-row mb-15"
|
||||||
|
>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabeledSelect
|
||||||
|
:value="row.fsType"
|
||||||
|
:label="t('harvester.virtualMachine.filesystem.type')"
|
||||||
|
:options="fsTypeOptions(index)"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
@update:value="onFsTypeChange(row, $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="row.volumeName"
|
||||||
|
:label="t('harvester.virtualMachine.filesystem.volume')"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-5">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="row.resourceName"
|
||||||
|
:label="t('harvester.virtualMachine.filesystem.resource')"
|
||||||
|
:options="resourceOptions(row.fsType)"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!isView"
|
||||||
|
class="col span-1 remove-col"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn role-link remove-btn"
|
||||||
|
@click="removeRow(index)"
|
||||||
|
>
|
||||||
|
{{ t('generic.remove') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-if="completedRows.length > 0 && mode === 'create'"
|
||||||
|
color="warning"
|
||||||
|
class="mt-10"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<MessageLink
|
||||||
|
:to="{ hash: '#advanced' }"
|
||||||
|
prefix-label="harvester.virtualMachine.filesystem.mountBannerHint"
|
||||||
|
middle-label="harvester.virtualMachine.filesystem.mountBannerHintLink"
|
||||||
|
suffix-label="harvester.virtualMachine.filesystem.mountBannerHintSuffix"
|
||||||
|
/>
|
||||||
|
<div class="pre-wrapper mt-10">
|
||||||
|
<pre class="mt-5 mb-0">{{ allMountCommands }}</pre>
|
||||||
|
<CopyToClipboard
|
||||||
|
:text="allMountCommands"
|
||||||
|
:show-label="false"
|
||||||
|
class="icon-btn"
|
||||||
|
action-color="bg-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Banner>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="!isView && canAddRow"
|
||||||
|
type="button"
|
||||||
|
class="btn role-tertiary add"
|
||||||
|
@click="addRow"
|
||||||
|
>
|
||||||
|
{{ t('harvester.virtualMachine.filesystem.add') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.vm-filesystem {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filesystem-row {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-col {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pre-wrapper {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
pre {
|
||||||
|
padding-right: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
right: 5px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--success) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -152,6 +152,10 @@ export default {
|
|||||||
return !rows.find((device) => !device.passthroughClaim);
|
return !rows.find((device) => !device.passthroughClaim);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
canManageGroup(rows = []) {
|
||||||
|
return rows.length > 0 && rows.every((row) => row.canUpdate === true);
|
||||||
|
},
|
||||||
|
|
||||||
changeRows(filterRows, parentSriov) {
|
changeRows(filterRows, parentSriov) {
|
||||||
this['filterRows'] = filterRows;
|
this['filterRows'] = filterRows;
|
||||||
this['parentSriov'] = parentSriov;
|
this['parentSriov'] = parentSriov;
|
||||||
@ -184,6 +188,10 @@ export default {
|
|||||||
:ref="group.key"
|
:ref="group.key"
|
||||||
v-trim-whitespace
|
v-trim-whitespace
|
||||||
class="group-tab"
|
class="group-tab"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="canManageGroup(group.rows)"
|
||||||
|
class="group-actions"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="groupIsAllEnabled(group.rows)"
|
v-if="groupIsAllEnabled(group.rows)"
|
||||||
@ -201,6 +209,7 @@ export default {
|
|||||||
>
|
>
|
||||||
{{ t('harvester.pci.enableGroup') }}
|
{{ t('harvester.pci.enableGroup') }}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
<span v-clean-html="group.key" />
|
<span v-clean-html="group.key" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -232,3 +241,9 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
</ResourceTable>
|
</ResourceTable>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.group-actions {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -52,31 +52,21 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectedDevices = [];
|
const selectedDevices = [];
|
||||||
const oldFormatDevices = [];
|
|
||||||
|
|
||||||
const vmDevices = this.value?.domain?.devices?.hostDevices || [];
|
const vmDevices = this.value?.domain?.devices?.hostDevices || [];
|
||||||
const otherDevices = this.otherDevices(vmDevices).map(({ name }) => name);
|
|
||||||
const vmDeviceNames = vmDevices.map(({ name }) => name);
|
const vmDeviceNames = vmDevices.map(({ name }) => name);
|
||||||
|
|
||||||
this.pciDevices.forEach((row) => {
|
this.pciDevices.forEach((row) => {
|
||||||
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
|
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
vmDevices.forEach(({ name, deviceName }) => {
|
vmDevices.forEach(({ name }) => {
|
||||||
const checkName = (deviceName || '').split('/')?.[1];
|
if (this.enabledDevices.find((device) => device?.metadata?.name === name)) {
|
||||||
|
|
||||||
if (checkName && name.includes(checkName) && !otherDevices.includes(name)) {
|
|
||||||
oldFormatDevices.push(name);
|
|
||||||
} else if (this.enabledDevices.find((device) => device?.metadata?.name === name)) {
|
|
||||||
selectedDevices.push(name);
|
selectedDevices.push(name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (oldFormatDevices.length > 0) {
|
|
||||||
this.oldFormatDevices = oldFormatDevices;
|
|
||||||
} else {
|
|
||||||
this.selectedDevices = selectedDevices;
|
this.selectedDevices = selectedDevices;
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@ -87,7 +77,6 @@ export default {
|
|||||||
selectedDevices: [],
|
selectedDevices: [],
|
||||||
pciDeviceSchema: this.$store.getters['harvester/schemaFor'](HCI.PCI_DEVICE),
|
pciDeviceSchema: this.$store.getters['harvester/schemaFor'](HCI.PCI_DEVICE),
|
||||||
showMatrix: false,
|
showMatrix: false,
|
||||||
oldFormatDevices: [],
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -199,11 +188,6 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
oldFormatDevicesHTML() {
|
|
||||||
return this.oldFormatDevices.map((device) => {
|
|
||||||
return `<li>${ device }</li>`;
|
|
||||||
}).join('');
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@ -227,17 +211,6 @@ export default {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
|
||||||
v-if="oldFormatDevices.length > 0"
|
|
||||||
class="row"
|
|
||||||
>
|
|
||||||
<div class="col span-12">
|
|
||||||
<Banner color="warning">
|
|
||||||
<p v-clean-html="t('harvester.pci.oldFormatDevices.help', {oldFormatDevicesHTML}, true)" />
|
|
||||||
</Banner>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col span-12">
|
<div class="col span-12">
|
||||||
<Banner color="info">
|
<Banner color="info">
|
||||||
@ -323,5 +296,4 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { _VIEW } from '@shell/config/query-params';
|
|||||||
|
|
||||||
import { NAMESPACE } from '@shell/config/types';
|
import { NAMESPACE } from '@shell/config/types';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
|
import { getLoginAwareErrors } from '../../utils/error';
|
||||||
|
|
||||||
const _NEW = '_NEW';
|
const _NEW = '_NEW';
|
||||||
|
|
||||||
@ -214,7 +215,9 @@ export default {
|
|||||||
buttonCb(true);
|
buttonCb(true);
|
||||||
this.cancel();
|
this.cancel();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.errors = [err.message];
|
const message = this.t('harvester.virtualMachine.genericLoginError');
|
||||||
|
|
||||||
|
this.errors = getLoginAwareErrors(err, message);
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -64,6 +64,12 @@ export default {
|
|||||||
value: 'status.productID',
|
value: 'status.productID',
|
||||||
sort: ['status.productID', 'status.vendorID']
|
sort: ['status.productID', 'status.vendorID']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'classType',
|
||||||
|
labelKey: 'harvester.usb.classType',
|
||||||
|
value: 'status.classType',
|
||||||
|
sort: ['status.classType']
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!isSingleProduct) {
|
if (!isSingleProduct) {
|
||||||
@ -107,6 +113,11 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
canManageGroup(rows = []) {
|
||||||
|
return rows.every((row) => row.canUpdate === true);
|
||||||
|
},
|
||||||
|
|
||||||
groupIsAllEnabled(rows = []) {
|
groupIsAllEnabled(rows = []) {
|
||||||
return !rows.find((device) => !device.passthroughClaim);
|
return !rows.find((device) => !device.passthroughClaim);
|
||||||
},
|
},
|
||||||
@ -146,6 +157,10 @@ export default {
|
|||||||
:ref="group.key"
|
:ref="group.key"
|
||||||
v-trim-whitespace
|
v-trim-whitespace
|
||||||
class="group-tab"
|
class="group-tab"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="canManageGroup(group.rows)"
|
||||||
|
class="group-actions"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="groupIsAllEnabled(group.rows)"
|
v-if="groupIsAllEnabled(group.rows)"
|
||||||
@ -163,6 +178,7 @@ export default {
|
|||||||
>
|
>
|
||||||
{{ t('harvester.usb.enableGroup') }}
|
{{ t('harvester.usb.enableGroup') }}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
<span v-clean-html="group.key" />
|
<span v-clean-html="group.key" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -175,3 +191,9 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
</ResourceTable>
|
</ResourceTable>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.group-actions {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -475,7 +475,6 @@ export default {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm bg-primary mr-15 mb-10"
|
class="btn btn-sm bg-primary mr-15 mb-10"
|
||||||
:disabled="rows.length === 0"
|
|
||||||
@click="addVolume(SOURCE_TYPE.NEW)"
|
@click="addVolume(SOURCE_TYPE.NEW)"
|
||||||
>
|
>
|
||||||
{{ t('harvester.virtualMachine.volume.addVolume') }}
|
{{ t('harvester.virtualMachine.volume.addVolume') }}
|
||||||
|
|||||||
@ -21,9 +21,7 @@ import { saferDump } from '@shell/utils/create-yaml';
|
|||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
import { BEFORE_SAVE_HOOKS, AFTER_SAVE_HOOKS } from '@shell/mixins/child-hook';
|
import { BEFORE_SAVE_HOOKS, AFTER_SAVE_HOOKS } from '@shell/mixins/child-hook';
|
||||||
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
|
||||||
import { parseVolumeClaimTemplates } from '@pkg/utils/vm';
|
import { parseVolumeClaimTemplates } from '@pkg/utils/vm';
|
||||||
import VM_MIXIN from '../../mixins/harvester-vm';
|
import VM_MIXIN from '../../mixins/harvester-vm';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
@ -37,6 +35,7 @@ import Network from './VirtualMachineNetwork';
|
|||||||
import Volume from './VirtualMachineVolume';
|
import Volume from './VirtualMachineVolume';
|
||||||
import SSHKey from './VirtualMachineSSHKey';
|
import SSHKey from './VirtualMachineSSHKey';
|
||||||
import Reserved from './VirtualMachineReserved';
|
import Reserved from './VirtualMachineReserved';
|
||||||
|
import Filesystem from './VirtualMachineFilesystem';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import MessageLink from '@shell/components/MessageLink';
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
|
|
||||||
@ -72,6 +71,7 @@ export default {
|
|||||||
Banner,
|
Banner,
|
||||||
MessageLink,
|
MessageLink,
|
||||||
UsbDevices,
|
UsbDevices,
|
||||||
|
Filesystem,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView, VM_MIXIN],
|
mixins: [CreateEditView, VM_MIXIN],
|
||||||
@ -91,6 +91,8 @@ export default {
|
|||||||
|
|
||||||
const hostname = this.value.spec.template.spec.hostname || '';
|
const hostname = this.value.spec.template.spec.hostname || '';
|
||||||
|
|
||||||
|
const customizeDisplayName = !!(this.value.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cloneVM,
|
cloneVM,
|
||||||
count: 2,
|
count: 2,
|
||||||
@ -101,12 +103,24 @@ export default {
|
|||||||
isOpen: false,
|
isOpen: false,
|
||||||
hostname,
|
hostname,
|
||||||
isRestartImmediately,
|
isRestartImmediately,
|
||||||
|
customizeDisplayName,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
// VM display name is stored as an annotation; bind a dedicated input to it
|
||||||
|
// so we don't have to mutate metadata.name (which would break the k8s PUT).
|
||||||
|
displayName: {
|
||||||
|
get() {
|
||||||
|
return this.value.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME] || '';
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.value.setAnnotation(HCI_ANNOTATIONS.VM_DISPLAY_NAME, val);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
to() {
|
to() {
|
||||||
return {
|
return {
|
||||||
name: 'harvester-c-cluster-resource',
|
name: 'harvester-c-cluster-resource',
|
||||||
@ -218,6 +232,9 @@ export default {
|
|||||||
usbPassthroughEnabled() {
|
usbPassthroughEnabled() {
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
|
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
|
||||||
},
|
},
|
||||||
|
filesystemEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('supportFilesystem');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@ -289,6 +306,12 @@ export default {
|
|||||||
this.getInitConfig({ value: this.value, init: this.isCreate });
|
this.getInitConfig({ value: this.value, init: this.isCreate });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
customizeDisplayName(neu) {
|
||||||
|
if (!neu) {
|
||||||
|
this.value.setAnnotation(HCI_ANNOTATIONS.VM_DISPLAY_NAME, '');
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
@ -321,6 +344,7 @@ export default {
|
|||||||
const diskRows = this.getDiskRows(this.value);
|
const diskRows = this.getDiskRows(this.value);
|
||||||
|
|
||||||
this['diskRows'] = diskRows;
|
this['diskRows'] = diskRows;
|
||||||
|
this['filesystemRows'] = this.getFilesystemRows(this.value);
|
||||||
const templateId = this.$route.query.templateId;
|
const templateId = this.$route.query.templateId;
|
||||||
const templateVersionId = this.$route.query.versionId;
|
const templateVersionId = this.$route.query.versionId;
|
||||||
|
|
||||||
@ -610,6 +634,33 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
</NameNsDescription>
|
</NameNsDescription>
|
||||||
|
|
||||||
|
<div v-if="isSingle">
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<Checkbox
|
||||||
|
v-model:value="customizeDisplayName"
|
||||||
|
class="check"
|
||||||
|
type="checkbox"
|
||||||
|
:label="t('harvester.virtualMachine.input.customizeDisplayName')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="customizeDisplayName"
|
||||||
|
class="row mb-20"
|
||||||
|
>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="displayName"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.virtualMachine.input.displayName')"
|
||||||
|
:placeholder="t('harvester.virtualMachine.input.displayNamePlaceholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-if="isCreate"
|
v-if="isCreate"
|
||||||
v-model:value="useTemplate"
|
v-model:value="useTemplate"
|
||||||
@ -783,10 +834,23 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
v-if="filesystemEnabled"
|
||||||
|
name="filesystem"
|
||||||
|
:label="t('harvester.tab.filesystem')"
|
||||||
|
:weight="-9"
|
||||||
|
>
|
||||||
|
<Filesystem
|
||||||
|
v-model:value="filesystemRows"
|
||||||
|
:mode="isCreate ? mode : 'view'"
|
||||||
|
:namespace="value.metadata.namespace"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
name="labels"
|
name="labels"
|
||||||
:label="t('generic.labels')"
|
:label="t('generic.labels')"
|
||||||
:weight="-9"
|
:weight="-10"
|
||||||
>
|
>
|
||||||
<Banner color="info">
|
<Banner color="info">
|
||||||
<t k="harvester.virtualMachine.labels.banner" />
|
<t k="harvester.virtualMachine.labels.banner" />
|
||||||
@ -805,7 +869,7 @@ export default {
|
|||||||
<Tab
|
<Tab
|
||||||
name="instanceLabel"
|
name="instanceLabel"
|
||||||
:label="t('harvester.tab.instanceLabel')"
|
:label="t('harvester.tab.instanceLabel')"
|
||||||
:weight="-10"
|
:weight="-11"
|
||||||
>
|
>
|
||||||
<Banner color="info">
|
<Banner color="info">
|
||||||
<t k="harvester.virtualMachine.instanceLabels.banner" />
|
<t k="harvester.virtualMachine.instanceLabels.banner" />
|
||||||
@ -826,7 +890,7 @@ export default {
|
|||||||
<Tab
|
<Tab
|
||||||
name="annotations"
|
name="annotations"
|
||||||
:label="t('harvester.tab.annotations')"
|
:label="t('harvester.tab.annotations')"
|
||||||
:weight="-11"
|
:weight="-12"
|
||||||
>
|
>
|
||||||
<Banner color="info">
|
<Banner color="info">
|
||||||
<t k="harvester.virtualMachine.annotations.banner" />
|
<t k="harvester.virtualMachine.annotations.banner" />
|
||||||
@ -847,7 +911,7 @@ export default {
|
|||||||
<Tab
|
<Tab
|
||||||
name="advanced"
|
name="advanced"
|
||||||
:label="t('harvester.tab.advanced')"
|
:label="t('harvester.tab.advanced')"
|
||||||
:weight="-12"
|
:weight="-13"
|
||||||
>
|
>
|
||||||
<div class="row mb-20">
|
<div class="row mb-20">
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
|
|||||||
419
pkg/harvester/edit/network.harvesterhci.io.hostnetworkconfig.vue
Normal file
419
pkg/harvester/edit/network.harvesterhci.io.hostnetworkconfig.vue
Normal file
@ -0,0 +1,419 @@
|
|||||||
|
<script>
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import InfoBox from '@shell/components/InfoBox';
|
||||||
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import HarvesterNodeSelector from '../components/HarvesterNodeSelector';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { set } from '@shell/utils/object';
|
||||||
|
import { NODE } from '@shell/config/types';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import { ADD_ONS } from '../config/harvester-map';
|
||||||
|
|
||||||
|
const MODE_DHCP = 'dhcp';
|
||||||
|
const MODE_STATIC = 'static';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterHostNetworkConfigEditPage',
|
||||||
|
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
CruResource,
|
||||||
|
NameNsDescription,
|
||||||
|
ResourceTabs,
|
||||||
|
Tab,
|
||||||
|
InfoBox,
|
||||||
|
MessageLink,
|
||||||
|
LabeledSelect,
|
||||||
|
LabeledInput,
|
||||||
|
RadioGroup,
|
||||||
|
Checkbox,
|
||||||
|
HarvesterNodeSelector,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
await allHash({
|
||||||
|
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
|
||||||
|
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
|
||||||
|
hostNetworkConfigs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HOST_NETWORK_CONFIG }),
|
||||||
|
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const networkMode = this.value?.spec?.mode || MODE_DHCP;
|
||||||
|
const ips = { ...(this.value?.spec?.ips || {}) };
|
||||||
|
|
||||||
|
if (!this.value.spec) {
|
||||||
|
set(this.value, 'spec', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
networkMode,
|
||||||
|
ips,
|
||||||
|
hasNodeSelector: !!this.value?.spec?.nodeSelector,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
modeOptions() {
|
||||||
|
return [
|
||||||
|
{ label: 'DHCP', value: MODE_DHCP },
|
||||||
|
{ label: 'Static', value: MODE_STATIC },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
clusterNetworkOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||||
|
|
||||||
|
return clusterNetworks.map((n) => {
|
||||||
|
const disabled = !n.isReady;
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: disabled ? `${ n.id } (${ this.t('generic.notReady') })` : n.id,
|
||||||
|
value: n.id,
|
||||||
|
disabled,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
nodes() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return this.$store.getters[`${ inStore }/all`](NODE) || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
isStaticMode() {
|
||||||
|
return this.networkMode === MODE_STATIC;
|
||||||
|
},
|
||||||
|
|
||||||
|
underlay: {
|
||||||
|
get() {
|
||||||
|
return !!this.value?.spec?.underlay;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
set(this.value, 'spec.underlay', val);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
vlanID: {
|
||||||
|
get() {
|
||||||
|
return this.value?.spec?.vlanID;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
set(this.value, 'spec.vlanID', val);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
clusterNetwork: {
|
||||||
|
get() {
|
||||||
|
return this.value?.spec?.clusterNetwork;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
set(this.value, 'spec.clusterNetwork', val);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
underlayConflict() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const all = this.$store.getters[`${ inStore }/all`](HCI.HOST_NETWORK_CONFIG) || [];
|
||||||
|
const currentId = this.value?.id;
|
||||||
|
|
||||||
|
return all.find((c) => c.id !== currentId && c.spec?.underlay === true) || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
kubeovnEnabled() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const addons = this.$store.getters[`${ inStore }/all`](HCI.ADD_ONS) || [];
|
||||||
|
|
||||||
|
return addons.find((a) => a.name === ADD_ONS.KUBEOVN_OPERATOR)?.spec?.enabled === true;
|
||||||
|
},
|
||||||
|
|
||||||
|
underlayDisabled() {
|
||||||
|
return !this.kubeovnEnabled || !!this.underlayConflict;
|
||||||
|
},
|
||||||
|
|
||||||
|
kubeovnAddonTo() {
|
||||||
|
return {
|
||||||
|
name: 'c-cluster-product-resource-namespace-id',
|
||||||
|
params: {
|
||||||
|
cluster: this.$route.params.cluster,
|
||||||
|
product: this.$store.getters['productId'],
|
||||||
|
resource: HCI.ADD_ONS,
|
||||||
|
namespace: 'kube-system',
|
||||||
|
id: ADD_ONS.KUBEOVN_OPERATOR,
|
||||||
|
},
|
||||||
|
query: { mode: 'edit' },
|
||||||
|
hash: '#basic',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
networkMode(neu) {
|
||||||
|
set(this.value, 'spec.mode', neu);
|
||||||
|
|
||||||
|
if (neu !== MODE_STATIC) {
|
||||||
|
if (this.value?.spec?.ips !== undefined) {
|
||||||
|
delete this.value.spec.ips;
|
||||||
|
}
|
||||||
|
this.ips = {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.updateBeforeSave);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updateBeforeSave() {
|
||||||
|
set(this.value, 'spec.mode', this.networkMode);
|
||||||
|
|
||||||
|
if (this.isStaticMode) {
|
||||||
|
set(this.value, 'spec.ips', { ...this.ips });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateIp(nodeName, val) {
|
||||||
|
this.ips = { ...this.ips, [nodeName]: val };
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeSelector() {
|
||||||
|
set(this.value.spec, 'nodeSelector', {
|
||||||
|
matchExpressions: [{
|
||||||
|
key: '', operator: 'In', values: []
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
this.hasNodeSelector = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
removeNodeSelector() {
|
||||||
|
delete this.value.spec.nodeSelector;
|
||||||
|
this.hasNodeSelector = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:mode="mode"
|
||||||
|
:resource="value"
|
||||||
|
:errors="errors"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
@finish="save"
|
||||||
|
@cancel="done"
|
||||||
|
@error="e => errors = e"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:namespaced="false"
|
||||||
|
description-key="spec.description"
|
||||||
|
@update:value="$emit('update:value', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ResourceTabs
|
||||||
|
class="mt-15"
|
||||||
|
:need-conditions="false"
|
||||||
|
:need-related="false"
|
||||||
|
:need-events="false"
|
||||||
|
:side-tabs="true"
|
||||||
|
:mode="mode"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="basic"
|
||||||
|
:label="t('harvester.hostNetworkConfig.tabs.mode')"
|
||||||
|
:weight="99"
|
||||||
|
class="bordered-table"
|
||||||
|
>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="networkMode"
|
||||||
|
name="hostNetworkConfigMode"
|
||||||
|
:options="modeOptions"
|
||||||
|
:mode="mode"
|
||||||
|
:row="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="clusterNetwork"
|
||||||
|
:label="t('harvester.network.clusterNetwork.label')"
|
||||||
|
:options="clusterNetworkOptions"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value.number="vlanID"
|
||||||
|
type="number"
|
||||||
|
required
|
||||||
|
:min="2"
|
||||||
|
:max="4094"
|
||||||
|
placeholder="e.g. 2 ~ 4094"
|
||||||
|
:label="t('harvester.hostNetworkConfig.vlanID.label')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<Checkbox
|
||||||
|
v-model:value="underlay"
|
||||||
|
:label="t('harvester.hostNetworkConfig.underlay.label')"
|
||||||
|
:tooltip="t('harvester.hostNetworkConfig.underlay.tooltip')"
|
||||||
|
:mode="mode"
|
||||||
|
:disabled="underlayDisabled"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="!kubeovnEnabled"
|
||||||
|
class="underlay-conflict-warning"
|
||||||
|
>
|
||||||
|
<i class="icon icon-warning" />
|
||||||
|
<MessageLink
|
||||||
|
:to="kubeovnAddonTo"
|
||||||
|
prefix-label="harvester.hostNetworkConfig.underlay.noKubeovn.prefix"
|
||||||
|
middle-label="harvester.hostNetworkConfig.underlay.noKubeovn.middle"
|
||||||
|
suffix-label="harvester.hostNetworkConfig.underlay.noKubeovn.suffix"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="underlayConflict"
|
||||||
|
class="underlay-conflict-warning"
|
||||||
|
>
|
||||||
|
<i class="icon icon-warning" />
|
||||||
|
{{ t('harvester.hostNetworkConfig.underlay.conflict', { name: underlayConflict.nameDisplay || underlayConflict.id }) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="isStaticMode">
|
||||||
|
<hr class="section-divider" />
|
||||||
|
<div
|
||||||
|
v-for="node in nodes"
|
||||||
|
:key="node.id"
|
||||||
|
class="row mb-10 ips-row"
|
||||||
|
>
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabeledInput
|
||||||
|
:value="node.nameDisplay || node.id"
|
||||||
|
:label="t('harvester.hostNetworkConfig.ips.nodeLabel')"
|
||||||
|
mode="view"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-5">
|
||||||
|
<LabeledInput
|
||||||
|
:value="ips[node.id]"
|
||||||
|
:label="t('harvester.hostNetworkConfig.ips.label')"
|
||||||
|
:placeholder="t('harvester.hostNetworkConfig.ips.placeholder')"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
@update:value="updateIp(node.id, $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
name="nodeSelector"
|
||||||
|
:label="t('harvester.hostNetworkConfig.tabs.nodeSelector')"
|
||||||
|
:weight="98"
|
||||||
|
>
|
||||||
|
<template v-if="hasNodeSelector">
|
||||||
|
<InfoBox class="node-selector-box">
|
||||||
|
<button
|
||||||
|
v-if="!isView"
|
||||||
|
type="button"
|
||||||
|
class="role-link btn btn-sm remove"
|
||||||
|
:aria-label="t('generic.remove')"
|
||||||
|
@click="removeNodeSelector"
|
||||||
|
>
|
||||||
|
<i class="icon icon-x" />
|
||||||
|
</button>
|
||||||
|
<HarvesterNodeSelector
|
||||||
|
class="mt-20"
|
||||||
|
:value="value.spec.nodeSelector"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</InfoBox>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn role-secondary"
|
||||||
|
:disabled="isView"
|
||||||
|
@click="addNodeSelector"
|
||||||
|
>
|
||||||
|
{{ t('harvester.hostNetworkConfig.nodeSelector.addButton') }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</Tab>
|
||||||
|
</ResourceTabs>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.section-divider {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
margin: 10px 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-selector-box {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.remove {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.underlay-conflict-warning {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--warning);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -31,6 +31,10 @@ export default {
|
|||||||
to() {
|
to() {
|
||||||
return this.vm?.detailLocation;
|
return this.vm?.detailLocation;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
attachVMName() {
|
||||||
|
return this.vm?.nameDisplay || this.vm?.metadata?.name || this.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -40,10 +44,10 @@ export default {
|
|||||||
v-if="to"
|
v-if="to"
|
||||||
:to="to"
|
:to="to"
|
||||||
>
|
>
|
||||||
{{ value }}
|
{{ attachVMName }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ value }}
|
{{ attachVMName }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
23
pkg/harvester/formatters/HarvesterBoolean.vue
Normal file
23
pkg/harvester/formatters/HarvesterBoolean.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterBooleanFormatter',
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span v-if="value">
|
||||||
|
<i class="icon icon-checkmark" />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-muted"
|
||||||
|
>
|
||||||
|
—
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
29
pkg/harvester/formatters/HarvesterHostNetworkConfigMode.vue
Normal file
29
pkg/harvester/formatters/HarvesterHostNetworkConfigMode.vue
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterHostNetworkConfigModeFormatter',
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
displayMode() {
|
||||||
|
if (this.value?.toLowerCase() === 'dhcp') {
|
||||||
|
return 'DHCP';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.value?.toLowerCase() === 'static') {
|
||||||
|
return 'Static';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span>{{ displayMode }}</span>
|
||||||
|
</template>
|
||||||
@ -20,12 +20,7 @@ export default {
|
|||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
vmiResource() {
|
vmiResource() {
|
||||||
const vmiList = this.$store.getters['harvester/all'](HCI.VMI) || [];
|
return this.$store.getters['harvester/byId'](HCI.VMI, this.vmResource?.id) || null;
|
||||||
const vmi = vmiList.find( (VMI) => {
|
|
||||||
return VMI?.metadata?.ownerReferences?.[0]?.uid === this.vmResource?.metadata?.uid;
|
|
||||||
});
|
|
||||||
|
|
||||||
return vmi;
|
|
||||||
},
|
},
|
||||||
migrationState() {
|
migrationState() {
|
||||||
return this.vmiResource?.migrationState?.status || '';
|
return this.vmiResource?.migrationState?.status || '';
|
||||||
|
|||||||
24
pkg/harvester/formatters/HarvesterVlan.vue
Normal file
24
pkg/harvester/formatters/HarvesterVlan.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterVlanFormatter',
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span v-if="value">
|
||||||
|
{{ value }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-muted"
|
||||||
|
>
|
||||||
|
—
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
@ -14,20 +14,6 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
|
||||||
allNodeNetwork: {
|
|
||||||
type: Array,
|
|
||||||
default: () => {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
allClusterNetwork: {
|
|
||||||
type: Array,
|
|
||||||
default: () => {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@ -144,6 +144,10 @@ harvester:
|
|||||||
migration:
|
migration:
|
||||||
failedMessage: Latest migration failed!
|
failedMessage: Latest migration failed!
|
||||||
title: Migration
|
title: Migration
|
||||||
|
vmMigrationTitle: '{count, plural, one {Migrating # VM} other {Migrating # VMs}}'
|
||||||
|
selectedVMs: "The following virtual machine(s) will be migrated to the target node"
|
||||||
|
unknownNode: (unknown node)
|
||||||
|
alreadyOnTarget: Already on Target
|
||||||
fields:
|
fields:
|
||||||
nodeName:
|
nodeName:
|
||||||
label: Target Node
|
label: Target Node
|
||||||
@ -248,6 +252,7 @@ harvester:
|
|||||||
suspendSchedule: Suspend
|
suspendSchedule: Suspend
|
||||||
restoreExistingVM: Replace Existing
|
restoreExistingVM: Replace Existing
|
||||||
migrate: Migrate
|
migrate: Migrate
|
||||||
|
vmMigrate: Virtual Machine Migration
|
||||||
cpuAndMemoryHotplug: Edit CPU and Memory
|
cpuAndMemoryHotplug: Edit CPU and Memory
|
||||||
abortMigration: Abort Migration
|
abortMigration: Abort Migration
|
||||||
storageMigration: Storage Migration
|
storageMigration: Storage Migration
|
||||||
@ -290,6 +295,12 @@ harvester:
|
|||||||
harvesterIpAddress:
|
harvesterIpAddress:
|
||||||
customIpTooltip: "Custom IP (set via annotation)"
|
customIpTooltip: "Custom IP (set via annotation)"
|
||||||
tableHeaders:
|
tableHeaders:
|
||||||
|
hostNetworkConfig:
|
||||||
|
underlay: Underlay
|
||||||
|
underlayTooltip: Allow this interface to act as the underlay for VM overlay networks.
|
||||||
|
vlanID: VLAN ID
|
||||||
|
mode: Mode
|
||||||
|
clusterNetwork: Cluster Network
|
||||||
imageEncryption: Encryption
|
imageEncryption: Encryption
|
||||||
size: Size
|
size: Size
|
||||||
virtualSize: Virtual Size
|
virtualSize: Virtual Size
|
||||||
@ -298,6 +309,7 @@ harvester:
|
|||||||
phase: Phase
|
phase: Phase
|
||||||
attachedVM: Attached Virtual Machine
|
attachedVM: Attached Virtual Machine
|
||||||
cpuManager: CPU Manager
|
cpuManager: CPU Manager
|
||||||
|
routeConnectivityTooltip: Connectivity between the VM network and the management network, which the Harvester nodes are connected to.
|
||||||
fingerprint: Fingerprint
|
fingerprint: Fingerprint
|
||||||
value: Value
|
value: Value
|
||||||
actions: Actions
|
actions: Actions
|
||||||
@ -336,6 +348,9 @@ harvester:
|
|||||||
vmImportSourceOClusterStatus: Cluster Status
|
vmImportSourceOClusterStatus: Cluster Status
|
||||||
vmImportSourceOVAUrl: URL
|
vmImportSourceOVAUrl: URL
|
||||||
vmImportSourceOVAStatus: Status
|
vmImportSourceOVAStatus: Status
|
||||||
|
v4ip: V4 IP
|
||||||
|
v6ip: V6 IP
|
||||||
|
eipName: EIP Name
|
||||||
tab:
|
tab:
|
||||||
volume: Volumes
|
volume: Volumes
|
||||||
network: Networks
|
network: Networks
|
||||||
@ -349,6 +364,7 @@ harvester:
|
|||||||
snapshots: Snapshots
|
snapshots: Snapshots
|
||||||
instanceLabel: Instance Labels
|
instanceLabel: Instance Labels
|
||||||
annotations: Annotations
|
annotations: Annotations
|
||||||
|
filesystem: Filesystem Volume
|
||||||
fields:
|
fields:
|
||||||
version: Version
|
version: Version
|
||||||
name: Name
|
name: Name
|
||||||
@ -385,21 +401,6 @@ harvester:
|
|||||||
middle: vGPU Devices
|
middle: vGPU Devices
|
||||||
suffix: page.
|
suffix: page.
|
||||||
deviceInTheSameHost: 'You can only select devices on the same host.'
|
deviceInTheSameHost: 'You can only select devices on the same host.'
|
||||||
oldFormatDevices:
|
|
||||||
help: |-
|
|
||||||
<p>
|
|
||||||
The following PCI devices are using the old naming convention and need to be updated in the YAML file:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
{oldFormatDevicesHTML}
|
|
||||||
</ul>
|
|
||||||
<p>
|
|
||||||
Please use the following instructions to update the virtual machine:
|
|
||||||
</p>
|
|
||||||
<ol>
|
|
||||||
<li>Stop the virtual machine, edit the virtual machine YAML, and remove the <Code>hostDevices</Code> section, and save virtual machine the changes to the YAML file.</li>
|
|
||||||
<li>Edit the virtual machine, and add the already enabled PCI Device from the list of available PCIDevices, and save and start VM.</li>
|
|
||||||
</ol>
|
|
||||||
showCompatibility: Show device compatibility matrix
|
showCompatibility: Show device compatibility matrix
|
||||||
hideCompatibility: Hide device compatibility matrix
|
hideCompatibility: Hide device compatibility matrix
|
||||||
claimError: Error enabling passthrough on {name}
|
claimError: Error enabling passthrough on {name}
|
||||||
@ -417,6 +418,8 @@ harvester:
|
|||||||
suffix: to enable the add-on to successfully manage your PCI devices.
|
suffix: to enable the add-on to successfully manage your PCI devices.
|
||||||
noPCIPermission: Please contact your system administrator to enable the PCI devices first.
|
noPCIPermission: Please contact your system administrator to enable the PCI devices first.
|
||||||
enablePassthroughWarning: Please be careful not to use host-owned PCI devices (e.g., management and VLAN NICs). Incorrect device allocation may cause damage to your cluster, including node failure.
|
enablePassthroughWarning: Please be careful not to use host-owned PCI devices (e.g., management and VLAN NICs). Incorrect device allocation may cause damage to your cluster, including node failure.
|
||||||
|
disableResourcePooling: Disable Resource Pooling
|
||||||
|
disableResourcePoolingDescription: Assigns this device a unique resource name so it can be pinned to a specific VM, instead of sharing the pool with other identical devices.
|
||||||
|
|
||||||
devices:
|
devices:
|
||||||
matrixHostName: Host Name
|
matrixHostName: Host Name
|
||||||
@ -726,6 +729,7 @@ harvester:
|
|||||||
other {Start}
|
other {Start}
|
||||||
} Now
|
} Now
|
||||||
createSSHKey: Create a New...
|
createSSHKey: Create a New...
|
||||||
|
genericLoginError: Authentication failed. Please re-log in and try again.
|
||||||
installAgent: Install guest agent
|
installAgent: Install guest agent
|
||||||
enableUsb: Enable USB Tablet
|
enableUsb: Enable USB Tablet
|
||||||
advancedOptions:
|
advancedOptions:
|
||||||
@ -780,7 +784,7 @@ harvester:
|
|||||||
addPort: Add Port
|
addPort: Add Port
|
||||||
cloudConfig:
|
cloudConfig:
|
||||||
title: Cloud Configuration
|
title: Cloud Configuration
|
||||||
createTemplateTitle: 'Create {name}.'
|
createTemplateTitle: 'Create {name}'
|
||||||
createNew: Create new...
|
createNew: Create new...
|
||||||
cloudInit:
|
cloudInit:
|
||||||
label: Cloud Init
|
label: Cloud Init
|
||||||
@ -823,6 +827,18 @@ harvester:
|
|||||||
username: Username
|
username: Username
|
||||||
password: Password
|
password: Password
|
||||||
reservedMemory: Reserved Memory
|
reservedMemory: Reserved Memory
|
||||||
|
customizeDisplayName: Customize virtual machine display name
|
||||||
|
displayNamePlaceholder: Virtual machine alias name
|
||||||
|
displayName: Display Name
|
||||||
|
filesystem:
|
||||||
|
description: Harvester supports filesystem volumes for VM via virtiofs.
|
||||||
|
type: Filesystem Type
|
||||||
|
volume: Volume
|
||||||
|
resource: Resource
|
||||||
|
add: Add
|
||||||
|
mountBannerHint: "Please update the mount path (e.g. /mnt/appconfigfs) to your preferred location, then add the corresponding commands to the"
|
||||||
|
mountBannerHintLink: "runcmd"
|
||||||
|
mountBannerHintSuffix: "in Advanced tab User Data."
|
||||||
machineTypeTip: 'Specify a processor architecture to emulate. To see a list of supported architectures, run: qemu-system-x86_64 -cpu ?'
|
machineTypeTip: 'Specify a processor architecture to emulate. To see a list of supported architectures, run: qemu-system-x86_64 -cpu ?'
|
||||||
detail:
|
detail:
|
||||||
tabs:
|
tabs:
|
||||||
@ -1111,6 +1127,7 @@ harvester:
|
|||||||
replaceExisting: Replace existing
|
replaceExisting: Replace existing
|
||||||
virtualMachineName: Virtual Machine Name
|
virtualMachineName: Virtual Machine Name
|
||||||
keepMacAddress: Keep MAC Address
|
keepMacAddress: Keep MAC Address
|
||||||
|
haltAfterRestore: Keep powered off after restore
|
||||||
matchTarget: The current backup target does not match the existing one.
|
matchTarget: The current backup target does not match the existing one.
|
||||||
progress:
|
progress:
|
||||||
details: Volume details
|
details: Volume details
|
||||||
@ -1141,6 +1158,9 @@ harvester:
|
|||||||
gateway:
|
gateway:
|
||||||
label: Gateway IP
|
label: Gateway IP
|
||||||
placeholder: e.g. 172.20.0.1
|
placeholder: e.g. 172.20.0.1
|
||||||
|
vlan:
|
||||||
|
label: VLAN
|
||||||
|
placeholder: Select a VLAN
|
||||||
dhcp:
|
dhcp:
|
||||||
label: Dynamic Host Configuration Protocol (DHCP)
|
label: Dynamic Host Configuration Protocol (DHCP)
|
||||||
v4Options: DHCPV4Options
|
v4Options: DHCPV4Options
|
||||||
@ -1148,6 +1168,9 @@ harvester:
|
|||||||
placeholder: key1=value1, key2=value2
|
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>
|
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.
|
tooltip: Enable DHCP server for this subnet. When enabled, VMs can automatically obtain IP addresses from this subnet.
|
||||||
|
externalConnectivity:
|
||||||
|
label: NAT Outgoing
|
||||||
|
tooltip: Enable NAT for VMs using this subnet to access external networks.
|
||||||
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.
|
||||||
@ -1223,14 +1246,97 @@ harvester:
|
|||||||
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.
|
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.
|
||||||
|
natGateway:
|
||||||
|
label: Gateways
|
||||||
|
internalTenantNetwork:
|
||||||
|
label: Internal Tenant Network
|
||||||
|
placeholder: Select a Virtual Machine Network
|
||||||
|
vpc:
|
||||||
|
label: VPC
|
||||||
|
placeholder: Select a VPC
|
||||||
|
subnet:
|
||||||
|
label: Subnet
|
||||||
|
placeholder: Select a subnet
|
||||||
|
lanIp:
|
||||||
|
label: LAN IP
|
||||||
|
placeholder: Enter LAN IP
|
||||||
|
externalSubnets:
|
||||||
|
label: External Subnets
|
||||||
|
addLabel: Add
|
||||||
|
placeholder: Select a subnet
|
||||||
|
externalIP:
|
||||||
|
label: External IPs
|
||||||
|
natGateway:
|
||||||
|
label: VpcNatGateway
|
||||||
|
placeholder: Select a VpcNatGateway
|
||||||
|
externalSubnet:
|
||||||
|
label: External Subnet
|
||||||
|
placeholder: Select an external subnet
|
||||||
|
v4ip:
|
||||||
|
label: V4 IP
|
||||||
|
placeholder: public ip from external subnet
|
||||||
|
snat:
|
||||||
|
label: Source Rules
|
||||||
|
eip:
|
||||||
|
label: EIP
|
||||||
|
placeholder: Select an external IP
|
||||||
|
internalCIDR:
|
||||||
|
label: Internal CIDR
|
||||||
|
placeholder: internal subnet CIDR
|
||||||
|
dnat:
|
||||||
|
label: Destination Rules
|
||||||
|
eip:
|
||||||
|
label: EIP
|
||||||
|
placeholder: Select an external IP
|
||||||
|
externalPort:
|
||||||
|
label: External Port
|
||||||
|
placeholder: port number
|
||||||
|
internalIp:
|
||||||
|
label: Internal IP
|
||||||
|
placeholder: internal IP address
|
||||||
|
internalPort:
|
||||||
|
label: Internal Port
|
||||||
|
placeholder: port number
|
||||||
|
protocol:
|
||||||
|
label: Protocol
|
||||||
|
placeholder: Select protocol (tcp or udp)
|
||||||
|
providerNetwork:
|
||||||
|
label: Provider Networks
|
||||||
|
defaultInterface:
|
||||||
|
label: Default Interface
|
||||||
|
placeholder: Select the interface the same as master interface of external overlay network
|
||||||
|
customInterfaces:
|
||||||
|
label: Custom Interfaces
|
||||||
|
addLabel: Add Custom Interface
|
||||||
|
interface:
|
||||||
|
label: Network Interface
|
||||||
|
placeholder: e.g. eth2
|
||||||
|
nodes:
|
||||||
|
label: Nodes
|
||||||
|
addLabel: Add Node
|
||||||
|
placeholder: Select a node
|
||||||
|
excludedNodes:
|
||||||
|
label: Excluded Nodes
|
||||||
|
addLabel: Add Excluded Node
|
||||||
|
placeholder: Select node to exclude from this provider network
|
||||||
|
vlanNetwork:
|
||||||
|
label: VLANs
|
||||||
|
vlan:
|
||||||
|
id:
|
||||||
|
label: VLAN ID
|
||||||
|
placeholder: "e.g. 1-4094"
|
||||||
|
provider:
|
||||||
|
label: Provider Network
|
||||||
|
placeholder: Select a provider network
|
||||||
networkPolicy:
|
networkPolicy:
|
||||||
label: Network Policies
|
label: 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.
|
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:
|
||||||
basics: Basics
|
basics: Basics
|
||||||
layer3Network: Route
|
layer3Network: Route
|
||||||
|
nic: Network Interface Card
|
||||||
clusterNetwork:
|
clusterNetwork:
|
||||||
label: Cluster Network
|
label: Cluster Network
|
||||||
create: Create a new cluster network
|
create: Create a new cluster network
|
||||||
@ -1285,6 +1391,13 @@ harvester:
|
|||||||
rancherCluster:
|
rancherCluster:
|
||||||
kubeConfig: Rancher KubeConfig
|
kubeConfig: Rancher KubeConfig
|
||||||
removeUpstreamClusterWhenNamespaceIsDeleted: Remove Upstream Cluster When Namespace Is Deleted
|
removeUpstreamClusterWhenNamespaceIsDeleted: Remove Upstream Cluster When Namespace Is Deleted
|
||||||
|
clusterPodSecurityStandard:
|
||||||
|
whitelistedNamespaces:
|
||||||
|
label: 'Whitelisted Namespaces'
|
||||||
|
privilegedNamespaces:
|
||||||
|
label: 'Privileged Namespaces'
|
||||||
|
restrictedNamespaces:
|
||||||
|
label: 'Restricted Namespaces'
|
||||||
storageNetwork:
|
storageNetwork:
|
||||||
range:
|
range:
|
||||||
placeholder: e.g. 172.16.0.0/24
|
placeholder: e.g. 172.16.0.0/24
|
||||||
@ -1292,6 +1405,7 @@ harvester:
|
|||||||
invalid: '"Range" is invalid.'
|
invalid: '"Range" is invalid.'
|
||||||
clusterNetwork: Cluster Network
|
clusterNetwork: Cluster Network
|
||||||
vlan: VLAN ID
|
vlan: VLAN ID
|
||||||
|
exclusiveVlan: Exclusive VLAN
|
||||||
exclude:
|
exclude:
|
||||||
label: Exclude IPs
|
label: Exclude IPs
|
||||||
placeholder: CIDR format, e.g. 172.16.0.10/32
|
placeholder: CIDR format, e.g. 172.16.0.10/32
|
||||||
@ -1508,6 +1622,7 @@ harvester:
|
|||||||
vmSnapshot:
|
vmSnapshot:
|
||||||
label: Virtual Machine Snapshots
|
label: Virtual Machine Snapshots
|
||||||
createText: Restore Snapshot
|
createText: Restore Snapshot
|
||||||
|
title: Restore Virtual Machine
|
||||||
snapshot: Snapshot
|
snapshot: Snapshot
|
||||||
|
|
||||||
storage:
|
storage:
|
||||||
@ -1515,6 +1630,7 @@ harvester:
|
|||||||
useDefault: Use the default storage
|
useDefault: Use the default storage
|
||||||
volumeEncryption: Volume Encryption
|
volumeEncryption: Volume Encryption
|
||||||
secret: Secret
|
secret: Secret
|
||||||
|
volumeExpansionCheckbox: Enable Expansion
|
||||||
migratable:
|
migratable:
|
||||||
label: Migratable
|
label: Migratable
|
||||||
numberOfReplicas:
|
numberOfReplicas:
|
||||||
@ -1607,8 +1723,9 @@ harvester:
|
|||||||
schedulingRules: Select node(s) matching rules
|
schedulingRules: Select node(s) matching rules
|
||||||
uplink:
|
uplink:
|
||||||
nics:
|
nics:
|
||||||
label: NICs
|
label: NIC
|
||||||
addLabel: Add NIC
|
addLabel: Add NIC
|
||||||
|
overlayWarning: The NIC selected here must match the NIC provided in the provider network.
|
||||||
placeholder: Select a NIC that is available on all the selected nodes
|
placeholder: Select a NIC that is available on all the selected nodes
|
||||||
validate:
|
validate:
|
||||||
available: NIC "{nic}" is not available on the selected nodes
|
available: NIC "{nic}" is not available on the selected nodes
|
||||||
@ -1764,6 +1881,8 @@ harvester:
|
|||||||
repository: Image Repository
|
repository: Image Repository
|
||||||
driver:
|
driver:
|
||||||
location: Driver Location
|
location: Driver Location
|
||||||
|
enable:
|
||||||
|
title: Enable NVIDIA Driver Toolkit
|
||||||
parsingSpecError:
|
parsingSpecError:
|
||||||
The field 'spec.valuesContent' has invalid format.
|
The field 'spec.valuesContent' has invalid format.
|
||||||
usbController:
|
usbController:
|
||||||
@ -1848,6 +1967,32 @@ harvester:
|
|||||||
addLabel: Add CIDR
|
addLabel: Add CIDR
|
||||||
range:
|
range:
|
||||||
addLabel: Add Range
|
addLabel: Add Range
|
||||||
|
hostNetworkConfig:
|
||||||
|
label: Host Networks
|
||||||
|
mode:
|
||||||
|
label: Mode
|
||||||
|
tabs:
|
||||||
|
mode: Mode
|
||||||
|
nodeSelector: Node Selector
|
||||||
|
nodeSelector:
|
||||||
|
addButton: Add Node Selector
|
||||||
|
underlay:
|
||||||
|
label: Underlay
|
||||||
|
tooltip: Allow this interface to act as the underlay for VM overlay networks.
|
||||||
|
conflict: '`{name}` host network config already has underlay enabled. Only one underlay is allowed in the cluster.'
|
||||||
|
noKubeovn:
|
||||||
|
prefix: The kubeovn-operator add-on is not enabled. Click
|
||||||
|
middle: here
|
||||||
|
suffix: to enable the add-on for overlay networking.
|
||||||
|
vlanID:
|
||||||
|
label: VLAN ID
|
||||||
|
ipRange:
|
||||||
|
label: IP Range ({node})
|
||||||
|
placeholder: e.g. 192.168.1.10/24
|
||||||
|
ips:
|
||||||
|
nodeLabel: Node
|
||||||
|
label: IP
|
||||||
|
placeholder: 'e.g. 192.168.1.10/24'
|
||||||
|
|
||||||
service:
|
service:
|
||||||
healthCheckPort:
|
healthCheckPort:
|
||||||
@ -1893,6 +2038,8 @@ harvester:
|
|||||||
|
|
||||||
migconfiguration:
|
migconfiguration:
|
||||||
label: vGPU MIG Configurations
|
label: vGPU MIG Configurations
|
||||||
|
status:
|
||||||
|
outOfSync: Out of Sync
|
||||||
infoBanner: To configure the MIG configuration, please disable it first and re-enable after editing the configuration.
|
infoBanner: To configure the MIG configuration, please disable it first and re-enable after editing the configuration.
|
||||||
profileSpec: Profile Specs
|
profileSpec: Profile Specs
|
||||||
profileStatus: Profile Status
|
profileStatus: Profile Status
|
||||||
@ -1955,6 +2102,7 @@ harvester:
|
|||||||
title: Cannot Disable Passthrough
|
title: Cannot Disable Passthrough
|
||||||
message: Please detach the device from the VM and save it first before disabling passthrough.
|
message: Please detach the device from the VM and save it first before disabling passthrough.
|
||||||
enablePassthroughWarning: 'Please re-enable the USB device if the device path changes in the following situations:<br/> 1) Re-plugging the USB device.<br/> 2) Rebooting the node.<br/><br/>An incorrect device path may cause passthrough to fail.'
|
enablePassthroughWarning: 'Please re-enable the USB device if the device path changes in the following situations:<br/> 1) Re-plugging the USB device.<br/> 2) Rebooting the node.<br/><br/>An incorrect device path may cause passthrough to fail.'
|
||||||
|
classType: Class Type
|
||||||
|
|
||||||
harvesterVlanConfigMigrateDialog:
|
harvesterVlanConfigMigrateDialog:
|
||||||
targetClusterNetwork:
|
targetClusterNetwork:
|
||||||
@ -2032,6 +2180,8 @@ advancedSettings:
|
|||||||
'harv-auto-rotate-rke2-certs': The certificate rotation mechanism relies on Rancher. Harvester will automatically update certificates generation to trigger rotation.
|
'harv-auto-rotate-rke2-certs': The certificate rotation mechanism relies on Rancher. Harvester will automatically update certificates generation to trigger rotation.
|
||||||
'harv-kubeconfig-default-token-ttl-minutes': 'TTL (in minutes) applied on Harvester administration kubeconfig files. Default is 0, which means to never expire.'
|
'harv-kubeconfig-default-token-ttl-minutes': 'TTL (in minutes) applied on Harvester administration kubeconfig files. Default is 0, which means to never expire.'
|
||||||
'harv-longhorn-v2-data-engine-enabled': 'Enable the Longhorn V2 data engine. Default is false. <ul><li>Changing this setting will restart RKE2 on all nodes. This will not affect running VM workloads.</li><li>If you see "not enough hugepages-2Mi capacity" errors when enabling this setting, wait a minute for the error to clear. If the error remains, reboot the affected node.</li></ul>'
|
'harv-longhorn-v2-data-engine-enabled': 'Enable the Longhorn V2 data engine. Default is false. <ul><li>Changing this setting will restart RKE2 on all nodes. This will not affect running VM workloads.</li><li>If you see "not enough hugepages-2Mi capacity" errors when enabling this setting, wait a minute for the error to clear. If the error remains, reboot the affected node.</li></ul>'
|
||||||
|
'harv-longhorn-v2-data-engine-hugepage-enabled': 'Enable hugepages when using the Longhorn V2 data engine. Default is true. Disabling hugepages reduces memory pressure on low-spec nodes and increases deployment flexibility. However, performance may be lower compared to running with hugepages.'
|
||||||
|
'harv-longhorn-v2-data-engine-memory-size': 'Configure the amount of memory allocated to the SPDK target daemon when using the Longhorn V2 data engine. Default is 2048 MiB.'
|
||||||
'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.'
|
||||||
@ -2039,6 +2189,7 @@ advancedSettings:
|
|||||||
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
|
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
|
||||||
'harv-kubevirt-migration': 'Configure cluster-wide KubeVirt live migration parameters.'
|
'harv-kubevirt-migration': 'Configure cluster-wide KubeVirt live migration parameters.'
|
||||||
'harv-instance-manager-resources': 'Configure resource percentage reservations for Longhorn instance manager V1 and V2. Valid instance manager CPU range between 0 - 40.'
|
'harv-instance-manager-resources': 'Configure resource percentage reservations for Longhorn instance manager V1 and V2. Valid instance manager CPU range between 0 - 40.'
|
||||||
|
'harv-cluster-pod-security-standard': 'Enforce Kubernetes Pod Security Standards (PSS) at the cluster level.'
|
||||||
|
|
||||||
typeLabel:
|
typeLabel:
|
||||||
kubevirt.io.virtualmachine: |-
|
kubevirt.io.virtualmachine: |-
|
||||||
@ -2101,6 +2252,36 @@ typeLabel:
|
|||||||
one { Virtual Private Cloud }
|
one { Virtual Private Cloud }
|
||||||
other { Virtual Private Clouds }
|
other { Virtual Private Clouds }
|
||||||
}
|
}
|
||||||
|
kubeovn.io.vlan: |-
|
||||||
|
{count, plural,
|
||||||
|
one { VLAN Network }
|
||||||
|
other { VLAN Networks }
|
||||||
|
}
|
||||||
|
kubeovn.io.providernetwork: |-
|
||||||
|
{count, plural,
|
||||||
|
one { Provider Network }
|
||||||
|
other { Provider Networks }
|
||||||
|
}
|
||||||
|
kubeovn.io.vpcnatgateway: |-
|
||||||
|
{count, plural,
|
||||||
|
one { NAT Gateway }
|
||||||
|
other { NAT Gateways }
|
||||||
|
}
|
||||||
|
kubeovn.io.iptablessnatrule: |-
|
||||||
|
{count, plural,
|
||||||
|
one { Source Rule }
|
||||||
|
other { Source Rules }
|
||||||
|
}
|
||||||
|
kubeovn.io.iptablesdnatrule: |-
|
||||||
|
{count, plural,
|
||||||
|
one { Destination Rule }
|
||||||
|
other { Destination Rules }
|
||||||
|
}
|
||||||
|
kubeovn.io.iptableseip: |-
|
||||||
|
{count, plural,
|
||||||
|
one { External IP }
|
||||||
|
other { External IPs }
|
||||||
|
}
|
||||||
networking.k8s.io.networkpolicy: |-
|
networking.k8s.io.networkpolicy: |-
|
||||||
{count, plural,
|
{count, plural,
|
||||||
one { Network Policy }
|
one { Network Policy }
|
||||||
@ -2161,16 +2342,17 @@ typeLabel:
|
|||||||
one { PCI Device }
|
one { PCI Device }
|
||||||
other { PCI Devices }
|
other { PCI Devices }
|
||||||
}
|
}
|
||||||
persistentvolumeclaim: |-
|
|
||||||
{count, plural,
|
|
||||||
one { Volume }
|
|
||||||
other { Volumes }
|
|
||||||
}
|
|
||||||
network.harvesterhci.io.clusternetwork: |-
|
network.harvesterhci.io.clusternetwork: |-
|
||||||
{count, plural,
|
{count, plural,
|
||||||
one { Cluster Network }
|
one { Cluster Network }
|
||||||
other { Cluster Networks }
|
other { Cluster Networks }
|
||||||
}
|
}
|
||||||
|
network.harvesterhci.io.hostnetworkconfig: |-
|
||||||
|
{count, plural,
|
||||||
|
one { Host Network }
|
||||||
|
other { Host Networks }
|
||||||
|
}
|
||||||
harvesterhci.io.addon: |-
|
harvesterhci.io.addon: |-
|
||||||
{count, plural,
|
{count, plural,
|
||||||
one { Add-on }
|
one { Add-on }
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { Banner } from '@components/Banner';
|
|||||||
import Loading from '@shell/components/Loading';
|
import Loading from '@shell/components/Loading';
|
||||||
import ResourceTable from '@shell/components/ResourceTable';
|
import ResourceTable from '@shell/components/ResourceTable';
|
||||||
import BadgeState from '@shell/components/formatter/BadgeStateFormatter';
|
import BadgeState from '@shell/components/formatter/BadgeStateFormatter';
|
||||||
|
|
||||||
import { NAME, AGE, NAMESPACE, STATE } from '@shell/config/table-headers';
|
import { NAME, AGE, NAMESPACE, STATE } from '@shell/config/table-headers';
|
||||||
import { NETWORK_ATTACHMENT, SCHEMA } from '@shell/config/types';
|
import { NETWORK_ATTACHMENT, SCHEMA } from '@shell/config/types';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
@ -113,6 +112,7 @@ export default {
|
|||||||
value: 'connectivity',
|
value: 'connectivity',
|
||||||
labelKey: 'tableHeaders.routeConnectivity',
|
labelKey: 'tableHeaders.routeConnectivity',
|
||||||
formatter: 'NetworkRouteConnectivity',
|
formatter: 'NetworkRouteConnectivity',
|
||||||
|
tooltip: 'harvester.tableHeaders.routeConnectivityTooltip',
|
||||||
formatterOpts: { arbitrary: true },
|
formatterOpts: { arbitrary: true },
|
||||||
width: 130,
|
width: 130,
|
||||||
},
|
},
|
||||||
@ -166,6 +166,7 @@ export default {
|
|||||||
:schema="schema"
|
:schema="schema"
|
||||||
:groupable="true"
|
:groupable="true"
|
||||||
:rows="filterRows"
|
:rows="filterRows"
|
||||||
|
:ignore-filter="true"
|
||||||
key-field="_key"
|
key-field="_key"
|
||||||
>
|
>
|
||||||
<template #cell:state="{row}">
|
<template #cell:state="{row}">
|
||||||
|
|||||||
@ -106,8 +106,8 @@ export default {
|
|||||||
name: 'AttachedVM',
|
name: 'AttachedVM',
|
||||||
labelKey: 'tableHeaders.attachedVM',
|
labelKey: 'tableHeaders.attachedVM',
|
||||||
type: 'attached',
|
type: 'attached',
|
||||||
value: 'spec.claimRef',
|
value: 'attachVMName',
|
||||||
sort: 'name',
|
sort: 'attachVMName',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'VolumeSnapshotCounts',
|
name: 'VolumeSnapshotCounts',
|
||||||
@ -134,8 +134,8 @@ export default {
|
|||||||
return row?.attachVM?.detailLocation;
|
return row?.attachVM?.detailLocation;
|
||||||
},
|
},
|
||||||
|
|
||||||
getVMName(row) {
|
getAttachedVMName(row) {
|
||||||
return row.attachVM?.metadata?.name || '';
|
return row.attachVMName || '';
|
||||||
},
|
},
|
||||||
|
|
||||||
isInternalStorageClass(storageClassName) {
|
isInternalStorageClass(storageClassName) {
|
||||||
@ -173,10 +173,10 @@ export default {
|
|||||||
<template #cell:AttachedVM="{row}">
|
<template #cell:AttachedVM="{row}">
|
||||||
<div>
|
<div>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="getVMName(row)"
|
v-if="getAttachedVMName(row)"
|
||||||
:to="goTo(row)"
|
:to="goTo(row)"
|
||||||
>
|
>
|
||||||
{{ getVMName(row) }}
|
{{ getAttachedVMName(row) }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -67,6 +67,13 @@ export default {
|
|||||||
NAMESPACE,
|
NAMESPACE,
|
||||||
CIDR_BLOCK,
|
CIDR_BLOCK,
|
||||||
PROTOCOL,
|
PROTOCOL,
|
||||||
|
{
|
||||||
|
name: 'vlan',
|
||||||
|
labelKey: 'harvester.subnet.vlan.label',
|
||||||
|
value: 'spec.vlan',
|
||||||
|
sort: 'spec.vlan',
|
||||||
|
formatter: 'HarvesterVlan',
|
||||||
|
},
|
||||||
PROVIDER,
|
PROVIDER,
|
||||||
AGE
|
AGE
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
import ResourceTable from '@shell/components/ResourceTable';
|
import ResourceTable from '@shell/components/ResourceTable';
|
||||||
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
|
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
|
||||||
import {
|
import {
|
||||||
@ -12,11 +13,18 @@ import { HCI } from '../types';
|
|||||||
import HarvesterVmState from '../formatters/HarvesterVmState';
|
import HarvesterVmState from '../formatters/HarvesterVmState';
|
||||||
import ConsoleBar from '../components/VMConsoleBar';
|
import ConsoleBar from '../components/VMConsoleBar';
|
||||||
|
|
||||||
|
const ENCRYPTED_VOLUME_TOOLTIP_KEYS = {
|
||||||
|
all: 'harvester.virtualMachine.volume.lockTooltip.all',
|
||||||
|
partial: 'harvester.virtualMachine.volume.lockTooltip.partial',
|
||||||
|
};
|
||||||
|
|
||||||
export const VM_HEADERS = [
|
export const VM_HEADERS = [
|
||||||
STATE,
|
STATE,
|
||||||
{
|
{
|
||||||
...NAME,
|
...NAME,
|
||||||
width: 350,
|
width: 350,
|
||||||
|
value: 'nameDisplay',
|
||||||
|
sort: ['nameDisplay'],
|
||||||
},
|
},
|
||||||
NAMESPACE,
|
NAMESPACE,
|
||||||
{
|
{
|
||||||
@ -93,19 +101,9 @@ export default {
|
|||||||
this.hasNode = true;
|
this.hasNode = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.NODE_NETWORK)) {
|
|
||||||
_hash.nodeNetworks = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.NODE_NETWORK });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.CLUSTER_NETWORK)) {
|
|
||||||
_hash.clusterNetworks = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK });
|
|
||||||
}
|
|
||||||
|
|
||||||
const hash = await allHash(_hash);
|
const hash = await allHash(_hash);
|
||||||
|
|
||||||
this.allVMs = hash.vms;
|
this.allVMs = hash.vms;
|
||||||
this.allNodeNetworks = hash.nodeNetworks || [];
|
|
||||||
this.allClusterNetworks = hash.clusterNetworks || [];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@ -113,14 +111,14 @@ export default {
|
|||||||
hasNode: false,
|
hasNode: false,
|
||||||
allVMs: [],
|
allVMs: [],
|
||||||
allVMIs: [],
|
allVMIs: [],
|
||||||
allNodeNetworks: [],
|
|
||||||
allClusterNetworks: [],
|
|
||||||
restartNotificationDisplayed: false,
|
restartNotificationDisplayed: false,
|
||||||
HCI
|
HCI
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters({ actionCb: 'action-menu/performCallbackData' }),
|
||||||
|
|
||||||
headers() {
|
headers() {
|
||||||
const restoreCol = {
|
const restoreCol = {
|
||||||
name: 'restoreProgress',
|
name: 'restoreProgress',
|
||||||
@ -163,6 +161,12 @@ export default {
|
|||||||
*/
|
*/
|
||||||
hasBackUpRestoreInProgress() {
|
hasBackUpRestoreInProgress() {
|
||||||
return !!this.rows.find((r) => r.restoreResource && !r.restoreResource.fromSnapshot && !r.restoreResource.isComplete);
|
return !!this.rows.find((r) => r.restoreResource && !r.restoreResource.fromSnapshot && !r.restoreResource.isComplete);
|
||||||
|
},
|
||||||
|
|
||||||
|
vmRestartRequiredNames() {
|
||||||
|
return this.allVMs
|
||||||
|
.filter((vm) => vm.isRestartRequired)
|
||||||
|
.map((vm) => vm.metadata.name);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -181,18 +185,17 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
allVMs: {
|
actionCb(neu) {
|
||||||
handler(neu) {
|
if (neu?.clearTableSelection) {
|
||||||
const vmNames = [];
|
this.$refs.resourceTable.clearSelection();
|
||||||
|
this.$store.dispatch('action-menu/clearCallbackData');
|
||||||
neu.forEach((vm) => {
|
|
||||||
if (vm.isRestartRequired) {
|
|
||||||
vmNames.push(vm.metadata.name);
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
|
||||||
|
vmRestartRequiredNames(vmNames) {
|
||||||
const count = vmNames.length;
|
const count = vmNames.length;
|
||||||
|
|
||||||
if ( count === 0 && this.restartNotificationDisplayed) {
|
if (count === 0 && this.restartNotificationDisplayed) {
|
||||||
this.restartNotificationDisplayed = false;
|
this.restartNotificationDisplayed = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -203,9 +206,7 @@ export default {
|
|||||||
if (this.restartNotificationDisplayed) {
|
if (this.restartNotificationDisplayed) {
|
||||||
this.$store.dispatch('growl/clear');
|
this.$store.dispatch('growl/clear');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (count > 0 && vmNames.length > 0) {
|
|
||||||
this.$store.dispatch('growl/warning', {
|
this.$store.dispatch('growl/warning', {
|
||||||
title: this.t('harvester.notification.restartRequired.title', { count }),
|
title: this.t('harvester.notification.restartRequired.title', { count }),
|
||||||
message: this.t('harvester.notification.restartRequired.message', { vmNames: vmNames.join(', ') }),
|
message: this.t('harvester.notification.restartRequired.message', { vmNames: vmNames.join(', ') }),
|
||||||
@ -213,21 +214,13 @@ export default {
|
|||||||
}, { root: true });
|
}, { root: true });
|
||||||
this.restartNotificationDisplayed = true;
|
this.restartNotificationDisplayed = true;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
deep: true,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
lockIconTooltipMessage(row) {
|
lockIconTooltipMessage(row) {
|
||||||
const message = '';
|
const key = ENCRYPTED_VOLUME_TOOLTIP_KEYS[row.encryptedVolumeType];
|
||||||
|
|
||||||
if (row.encryptedVolumeType === 'all') {
|
return key ? this.t(key) : '';
|
||||||
return this.t('harvester.virtualMachine.volume.lockTooltip.all');
|
|
||||||
} else if (row.encryptedVolumeType === 'partial') {
|
|
||||||
return this.t('harvester.virtualMachine.volume.lockTooltip.partial');
|
|
||||||
}
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -237,6 +230,7 @@ export default {
|
|||||||
<Loading v-if="$fetchState.pending" />
|
<Loading v-if="$fetchState.pending" />
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<ResourceTable
|
<ResourceTable
|
||||||
|
ref="resourceTable"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
default-sort-by="age"
|
default-sort-by="age"
|
||||||
@ -253,8 +247,6 @@ export default {
|
|||||||
<HarvesterVmState
|
<HarvesterVmState
|
||||||
class="vmstate"
|
class="vmstate"
|
||||||
:row="scope.row"
|
:row="scope.row"
|
||||||
:all-node-network="allNodeNetworks"
|
|
||||||
:all-cluster-network="allClusterNetworks"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -265,16 +257,16 @@ export default {
|
|||||||
v-if="scope.row.type !== HCI.VMI"
|
v-if="scope.row.type !== HCI.VMI"
|
||||||
:to="scope.row.detailLocation"
|
:to="scope.row.detailLocation"
|
||||||
>
|
>
|
||||||
{{ scope.row.metadata.name }}
|
{{ scope.row.nameDisplay }}
|
||||||
<i
|
<i
|
||||||
v-if="lockIconTooltipMessage(scope.row)"
|
v-if="scope.row.encryptedVolumeType !== 'none'"
|
||||||
v-tooltip="lockIconTooltipMessage(scope.row)"
|
v-tooltip="lockIconTooltipMessage(scope.row)"
|
||||||
class="icon icon-lock"
|
class="icon icon-lock"
|
||||||
:class="{'green-icon': scope.row.encryptedVolumeType === 'all', 'yellow-icon': scope.row.encryptedVolumeType === 'partial'}"
|
:class="{'green-icon': scope.row.encryptedVolumeType === 'all', 'yellow-icon': scope.row.encryptedVolumeType === 'partial'}"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ scope.row.metadata.name }}
|
{{ scope.row.nameDisplay }}
|
||||||
</span>
|
</span>
|
||||||
<ConsoleBar
|
<ConsoleBar
|
||||||
:resource-type="scope.row"
|
:resource-type="scope.row"
|
||||||
|
|||||||
@ -0,0 +1,86 @@
|
|||||||
|
<script>
|
||||||
|
import ResourceTable from '@shell/components/ResourceTable';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import { STATE, NAME as NAME_COL, AGE } from '@shell/config/table-headers';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
|
const UNDERLAY = {
|
||||||
|
name: 'underlay',
|
||||||
|
labelKey: 'harvester.tableHeaders.hostNetworkConfig.underlay',
|
||||||
|
tooltip: 'harvester.tableHeaders.hostNetworkConfig.underlayTooltip',
|
||||||
|
value: 'spec.underlay',
|
||||||
|
sort: 'spec.underlay',
|
||||||
|
formatter: 'HarvesterBoolean',
|
||||||
|
};
|
||||||
|
|
||||||
|
const VLAN_ID = {
|
||||||
|
name: 'vlanID',
|
||||||
|
labelKey: 'harvester.tableHeaders.hostNetworkConfig.vlanID',
|
||||||
|
value: 'spec.vlanID',
|
||||||
|
sort: 'spec.vlanID',
|
||||||
|
};
|
||||||
|
|
||||||
|
const MODE = {
|
||||||
|
name: 'mode',
|
||||||
|
labelKey: 'harvester.tableHeaders.hostNetworkConfig.mode',
|
||||||
|
value: 'spec.mode',
|
||||||
|
sort: 'spec.mode',
|
||||||
|
formatter: 'HarvesterHostNetworkConfigMode',
|
||||||
|
};
|
||||||
|
|
||||||
|
const CLUSTER_NETWORK = {
|
||||||
|
name: 'clusterNetwork',
|
||||||
|
labelKey: 'harvester.tableHeaders.hostNetworkConfig.clusterNetwork',
|
||||||
|
value: 'spec.clusterNetwork',
|
||||||
|
sort: 'spec.clusterNetwork',
|
||||||
|
align: 'center',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterListHostNetworkConfig',
|
||||||
|
components: { ResourceTable, Loading },
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HOST_NETWORK_CONFIG });
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { rows: [] };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
schema() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return this.$store.getters[`${ inStore }/schemaFor`](HCI.HOST_NETWORK_CONFIG);
|
||||||
|
},
|
||||||
|
headers() {
|
||||||
|
return [
|
||||||
|
STATE,
|
||||||
|
NAME_COL,
|
||||||
|
UNDERLAY,
|
||||||
|
VLAN_ID,
|
||||||
|
MODE,
|
||||||
|
CLUSTER_NETWORK,
|
||||||
|
AGE,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<ResourceTable
|
||||||
|
v-else
|
||||||
|
v-bind="$attrs"
|
||||||
|
:headers="headers"
|
||||||
|
:rows="rows"
|
||||||
|
:schema="schema"
|
||||||
|
key-field="_key"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@ -12,7 +12,7 @@ import { base64Decode } from '@shell/utils/crypto';
|
|||||||
import { formatSi, parseSi } from '@shell/utils/units';
|
import { formatSi, parseSi } from '@shell/utils/units';
|
||||||
import { _CLONE, _CREATE, _VIEW } from '@shell/config/query-params';
|
import { _CLONE, _CREATE, _VIEW } from '@shell/config/query-params';
|
||||||
import {
|
import {
|
||||||
PV, PVC, STORAGE_CLASS, NODE, SECRET, CONFIG_MAP, NETWORK_ATTACHMENT, NAMESPACE, LONGHORN
|
PV, PVC, STORAGE_CLASS, NODE, SECRET, CONFIG_MAP, SERVICE_ACCOUNT, NETWORK_ATTACHMENT, NAMESPACE, LONGHORN
|
||||||
} from '@shell/config/types';
|
} from '@shell/config/types';
|
||||||
import { HOSTNAME } from '@shell/config/labels-annotations';
|
import { HOSTNAME } from '@shell/config/labels-annotations';
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
@ -25,7 +25,7 @@ import { HCI } from '../../types';
|
|||||||
import { parseVolumeClaimTemplates, EMPTY_IMAGE } from '../../utils/vm';
|
import { parseVolumeClaimTemplates, EMPTY_IMAGE } from '../../utils/vm';
|
||||||
import impl, { QGA_JSON, USB_TABLET } from './impl';
|
import impl, { QGA_JSON, USB_TABLET } from './impl';
|
||||||
import { GIBIBYTE } from '../../utils/unit';
|
import { GIBIBYTE } from '../../utils/unit';
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE, FILESYSTEM_SOURCE_TYPE } from '@pkg/harvester/config/types';
|
||||||
|
|
||||||
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
|
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
|
||||||
|
|
||||||
@ -102,6 +102,8 @@ export default {
|
|||||||
vmims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIM }),
|
vmims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIM }),
|
||||||
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
|
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
|
||||||
secrets: this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }),
|
secrets: this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }),
|
||||||
|
configMaps: this.$store.dispatch(`${ inStore }/findAll`, { type: CONFIG_MAP }),
|
||||||
|
serviceAccounts: this.$store.dispatch(`${ inStore }/findAll`, { type: SERVICE_ACCOUNT }),
|
||||||
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
|
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
|
||||||
longhornV2Engine: this.$store.dispatch(`${ inStore }/find`, { type: LONGHORN.SETTINGS, id: LONGHORN_V2_DATA_ENGINE }),
|
longhornV2Engine: this.$store.dispatch(`${ inStore }/find`, { type: LONGHORN.SETTINGS, id: LONGHORN_V2_DATA_ENGINE }),
|
||||||
};
|
};
|
||||||
@ -156,6 +158,7 @@ export default {
|
|||||||
imageId: '',
|
imageId: '',
|
||||||
diskRows: [],
|
diskRows: [],
|
||||||
networkRows: [],
|
networkRows: [],
|
||||||
|
filesystemRows: [],
|
||||||
machineType: '',
|
machineType: '',
|
||||||
machineTypes: [],
|
machineTypes: [],
|
||||||
secretName: '',
|
secretName: '',
|
||||||
@ -440,6 +443,7 @@ export default {
|
|||||||
this['imageId'] = imageId;
|
this['imageId'] = imageId;
|
||||||
|
|
||||||
this['diskRows'] = diskRows;
|
this['diskRows'] = diskRows;
|
||||||
|
this['filesystemRows'] = this.getFilesystemRows(vm);
|
||||||
|
|
||||||
this.refreshYamlEditor();
|
this.refreshYamlEditor();
|
||||||
},
|
},
|
||||||
@ -641,6 +645,80 @@ export default {
|
|||||||
this.parseAccessCredentials();
|
this.parseAccessCredentials();
|
||||||
this.parseNetworkRows(this.networkRows);
|
this.parseNetworkRows(this.networkRows);
|
||||||
this.parseDiskRows(this.diskRows);
|
this.parseDiskRows(this.diskRows);
|
||||||
|
this.parseFilesystemRows();
|
||||||
|
},
|
||||||
|
|
||||||
|
getFilesystemRows(vm) {
|
||||||
|
const _filesystems = vm.spec.template.spec.domain.devices?.filesystems || [];
|
||||||
|
const _volumes = vm.spec.template.spec.volumes || [];
|
||||||
|
|
||||||
|
return _filesystems.map((fs) => {
|
||||||
|
const volume = _volumes.find((v) => v.name === fs.name);
|
||||||
|
let fsType = FILESYSTEM_SOURCE_TYPE.CONFIGMAP;
|
||||||
|
let resourceName = '';
|
||||||
|
|
||||||
|
if (volume?.configMap) {
|
||||||
|
fsType = FILESYSTEM_SOURCE_TYPE.CONFIGMAP;
|
||||||
|
resourceName = volume.configMap.name;
|
||||||
|
} else if (volume?.secret) {
|
||||||
|
fsType = FILESYSTEM_SOURCE_TYPE.SECRET;
|
||||||
|
resourceName = volume.secret.secretName;
|
||||||
|
} else if (volume?.serviceAccount) {
|
||||||
|
fsType = FILESYSTEM_SOURCE_TYPE.SERVICEACCOUNT;
|
||||||
|
resourceName = volume.serviceAccount.serviceAccountName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fsType,
|
||||||
|
volumeName: fs.name,
|
||||||
|
resourceName,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
parseFilesystemRows() {
|
||||||
|
const completedRows = this.filesystemRows.filter(
|
||||||
|
(r) => r.fsType && r.volumeName && r.resourceName
|
||||||
|
);
|
||||||
|
|
||||||
|
const filesystems = completedRows.map((r) => ({
|
||||||
|
name: r.volumeName,
|
||||||
|
virtiofs: {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const fsVolumes = completedRows.map((r) => {
|
||||||
|
if (r.fsType === FILESYSTEM_SOURCE_TYPE.CONFIGMAP) {
|
||||||
|
return {
|
||||||
|
name: r.volumeName,
|
||||||
|
configMap: { name: r.resourceName },
|
||||||
|
};
|
||||||
|
} else if (r.fsType === FILESYSTEM_SOURCE_TYPE.SECRET) {
|
||||||
|
return {
|
||||||
|
name: r.volumeName,
|
||||||
|
secret: { secretName: r.resourceName },
|
||||||
|
};
|
||||||
|
} else if (r.fsType === FILESYSTEM_SOURCE_TYPE.SERVICEACCOUNT) {
|
||||||
|
return {
|
||||||
|
name: r.volumeName,
|
||||||
|
serviceAccount: { serviceAccountName: r.resourceName },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
if (filesystems.length > 0) {
|
||||||
|
this.spec.template.spec.domain.devices['filesystems'] = filesystems;
|
||||||
|
} else {
|
||||||
|
delete this.spec.template.spec.domain.devices['filesystems'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fsVolumes.length > 0) {
|
||||||
|
if (!this.spec.template.spec.volumes) {
|
||||||
|
this.spec.template.spec['volumes'] = [];
|
||||||
|
}
|
||||||
|
this.spec.template.spec.volumes.push(...fsVolumes);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
parseOther() {
|
parseOther() {
|
||||||
@ -907,14 +985,22 @@ export default {
|
|||||||
const specInterfaces = this.spec?.template?.spec?.domain?.devices?.interfaces;
|
const specInterfaces = this.spec?.template?.spec?.domain?.devices?.interfaces;
|
||||||
const mergedInterfaces = this.mergeInterfaceList(specInterfaces, interfaces);
|
const mergedInterfaces = this.mergeInterfaceList(specInterfaces, interfaces);
|
||||||
|
|
||||||
|
const devices = {
|
||||||
|
...this.spec.template.spec.domain.devices,
|
||||||
|
interfaces: mergedInterfaces,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.isEdit && networkRow.length === 0) {
|
||||||
|
devices.autoattachPodInterface = false;
|
||||||
|
} else {
|
||||||
|
delete devices.autoattachPodInterface;
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
interfaces: mergedInterfaces,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
networks
|
networks
|
||||||
};
|
};
|
||||||
@ -1656,7 +1742,7 @@ export default {
|
|||||||
|
|
||||||
const oldImageId = old[0]?.image;
|
const oldImageId = old[0]?.image;
|
||||||
|
|
||||||
if (this.isCreate && oldImageId === imageId && imageId) {
|
if (this.isCreate && oldImageId !== imageId && imageId && osType) {
|
||||||
this.osType = osType;
|
this.osType = osType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import HarvesterResource from './harvester';
|
|||||||
export default class MIGCONFIGURATION extends HarvesterResource {
|
export default class MIGCONFIGURATION extends HarvesterResource {
|
||||||
get _availableActions() {
|
get _availableActions() {
|
||||||
let out = super._availableActions;
|
let out = super._availableActions;
|
||||||
|
const canUpdate = !!this.linkFor('update');
|
||||||
|
|
||||||
out = out.map((action) => {
|
out = out.map((action) => {
|
||||||
if (action.action === 'showConfiguration') {
|
if (action.action === 'showConfiguration') {
|
||||||
@ -26,13 +27,13 @@ export default class MIGCONFIGURATION extends HarvesterResource {
|
|||||||
out.push(
|
out.push(
|
||||||
{
|
{
|
||||||
action: 'enableConfig',
|
action: 'enableConfig',
|
||||||
enabled: !this.isEnabled,
|
enabled: !this.isEnabled && canUpdate,
|
||||||
icon: 'icon icon-fw icon-dot',
|
icon: 'icon icon-fw icon-dot',
|
||||||
label: 'Enable',
|
label: 'Enable',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'disableConfig',
|
action: 'disableConfig',
|
||||||
enabled: this.isEnabled,
|
enabled: this.isEnabled && canUpdate,
|
||||||
icon: 'icon icon-fw icon-dot-open',
|
icon: 'icon icon-fw icon-dot-open',
|
||||||
label: 'Disable',
|
label: 'Disable',
|
||||||
},
|
},
|
||||||
@ -62,13 +63,27 @@ export default class MIGCONFIGURATION extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get stateDisplay() {
|
get stateDisplay() {
|
||||||
|
if (this.configStatus === 'out-of-sync') {
|
||||||
|
return this.t('harvester.migconfiguration.status.outOfSync');
|
||||||
|
}
|
||||||
|
|
||||||
return this.actualState;
|
return this.actualState;
|
||||||
}
|
}
|
||||||
|
|
||||||
get stateColor() {
|
get stateDescription() {
|
||||||
const state = this.actualState;
|
if (this.status?.message) {
|
||||||
|
return this.status.message;
|
||||||
|
}
|
||||||
|
|
||||||
return colorForState(state);
|
return super.stateDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
get stateColor() {
|
||||||
|
if (this.configStatus === 'out-of-sync') {
|
||||||
|
return 'text-warning';
|
||||||
|
}
|
||||||
|
|
||||||
|
return colorForState(this.actualState);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isEnabled() {
|
get isEnabled() {
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export default class PCIDevice extends SteveModel {
|
|||||||
out.push(
|
out.push(
|
||||||
{
|
{
|
||||||
action: 'enablePassthroughBulk',
|
action: 'enablePassthroughBulk',
|
||||||
enabled: !this.isEnabling && !this.isvGPUDevice,
|
enabled: !this.isEnabling && !this.isvGPUDevice && this.canUpdate,
|
||||||
icon: 'icon icon-fw icon-dot',
|
icon: 'icon icon-fw icon-dot',
|
||||||
label: 'Enable Passthrough',
|
label: 'Enable Passthrough',
|
||||||
bulkable: true,
|
bulkable: true,
|
||||||
@ -43,7 +43,7 @@ export default class PCIDevice extends SteveModel {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'disablePassthrough',
|
action: 'disablePassthrough',
|
||||||
enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice,
|
enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice && this.canUpdate,
|
||||||
icon: 'icon icon-fw icon-dot-open',
|
icon: 'icon icon-fw icon-dot-open',
|
||||||
label: 'Disable Passthrough',
|
label: 'Disable Passthrough',
|
||||||
bulkable: true,
|
bulkable: true,
|
||||||
@ -54,6 +54,10 @@ export default class PCIDevice extends SteveModel {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canUpdate() {
|
||||||
|
return !!this.linkFor('update');
|
||||||
|
}
|
||||||
|
|
||||||
get isvGPUDevice() {
|
get isvGPUDevice() {
|
||||||
if (!this.vGPUAsPCIDeviceFeatureEnabled) {
|
if (!this.vGPUAsPCIDeviceFeatureEnabled) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -16,13 +16,13 @@ export default class SRIOVDevice extends SteveModel {
|
|||||||
out.push(
|
out.push(
|
||||||
{
|
{
|
||||||
action: 'enableDevice',
|
action: 'enableDevice',
|
||||||
enabled: !this.isEnabled,
|
enabled: !this.isEnabled && this.canUpdate,
|
||||||
icon: 'icon icon-fw icon-dot',
|
icon: 'icon icon-fw icon-dot',
|
||||||
label: 'Enable',
|
label: 'Enable',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'disableDevice',
|
action: 'disableDevice',
|
||||||
enabled: this.isEnabled,
|
enabled: this.isEnabled && this.canUpdate,
|
||||||
icon: 'icon icon-fw icon-dot-open',
|
icon: 'icon icon-fw icon-dot-open',
|
||||||
label: 'Disable',
|
label: 'Disable',
|
||||||
},
|
},
|
||||||
@ -31,6 +31,10 @@ export default class SRIOVDevice extends SteveModel {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canUpdate() {
|
||||||
|
return !!this.linkFor('update');
|
||||||
|
}
|
||||||
|
|
||||||
get canYaml() {
|
get canYaml() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export default class USBDevice extends SteveModel {
|
|||||||
out.push(
|
out.push(
|
||||||
{
|
{
|
||||||
action: 'enablePassthroughBulk',
|
action: 'enablePassthroughBulk',
|
||||||
enabled: !this.passthroughClaim && !this.status.enabled,
|
enabled: !this.passthroughClaim && !this.status.enabled && this.canUpdate,
|
||||||
icon: 'icon icon-fw icon-dot',
|
icon: 'icon icon-fw icon-dot',
|
||||||
label: 'Enable Passthrough',
|
label: 'Enable Passthrough',
|
||||||
bulkable: true,
|
bulkable: true,
|
||||||
@ -42,7 +42,7 @@ export default class USBDevice extends SteveModel {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'disablePassthrough',
|
action: 'disablePassthrough',
|
||||||
enabled: this.status.enabled,
|
enabled: this.status.enabled && this.canUpdate,
|
||||||
icon: 'icon icon-fw icon-dot-open',
|
icon: 'icon icon-fw icon-dot-open',
|
||||||
label: 'Disable Passthrough',
|
label: 'Disable Passthrough',
|
||||||
bulkable: true,
|
bulkable: true,
|
||||||
@ -53,6 +53,10 @@ export default class USBDevice extends SteveModel {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canUpdate() {
|
||||||
|
return !!this.linkFor('update');
|
||||||
|
}
|
||||||
|
|
||||||
get canYaml() {
|
get canYaml() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,17 +27,18 @@ const STATUS_DISPLAY = {
|
|||||||
export default class VGpuDevice extends SteveModel {
|
export default class VGpuDevice extends SteveModel {
|
||||||
get _availableActions() {
|
get _availableActions() {
|
||||||
const out = super._availableActions;
|
const out = super._availableActions;
|
||||||
|
const canUpdate = !!this.linkFor('update');
|
||||||
|
|
||||||
out.push(
|
out.push(
|
||||||
{
|
{
|
||||||
action: 'enableVGpu',
|
action: 'enableVGpu',
|
||||||
enabled: !this.isEnabled,
|
enabled: !this.isEnabled && canUpdate,
|
||||||
icon: 'icon icon-fw icon-dot',
|
icon: 'icon icon-fw icon-dot',
|
||||||
label: 'Enable',
|
label: 'Enable',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'disableVGpu',
|
action: 'disableVGpu',
|
||||||
enabled: this.isEnabled,
|
enabled: this.isEnabled && canUpdate,
|
||||||
icon: 'icon icon-fw icon-dot-open',
|
icon: 'icon icon-fw icon-dot-open',
|
||||||
label: 'Disable',
|
label: 'Disable',
|
||||||
bulkable: true,
|
bulkable: true,
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const OBSCURE_NAMESPACE_PREFIX = [
|
|||||||
|
|
||||||
export default class HciNamespace extends namespace {
|
export default class HciNamespace extends namespace {
|
||||||
get _availableActions() {
|
get _availableActions() {
|
||||||
const out = super._availableActions;
|
let out = super._availableActions;
|
||||||
const remove = out.findIndex((a) => a.action === 'promptRemove');
|
const remove = out.findIndex((a) => a.action === 'promptRemove');
|
||||||
|
|
||||||
const promptRemove = {
|
const promptRemove = {
|
||||||
@ -53,6 +53,16 @@ export default class HciNamespace extends namespace {
|
|||||||
insertAt(out, out.length - 1, promptRemove);
|
insertAt(out, out.length - 1, promptRemove);
|
||||||
insertAt(out, out.length - 5, editQuotaAction);
|
insertAt(out, out.length - 5, editQuotaAction);
|
||||||
|
|
||||||
|
const canUpdate = !!this.linkFor('update');
|
||||||
|
|
||||||
|
out = out.map((action) => {
|
||||||
|
if (['move'].includes(action.action)) {
|
||||||
|
return { ...action, enabled: action.enabled && canUpdate };
|
||||||
|
}
|
||||||
|
|
||||||
|
return action;
|
||||||
|
});
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -235,6 +235,10 @@ export default class HciPv extends HarvesterResource {
|
|||||||
return allVMs.find(findAttachVM);
|
return allVMs.find(findAttachVM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get attachVMName() {
|
||||||
|
return this.attachVM?.nameDisplay || this.attachVM?.metadata?.name || '';
|
||||||
|
}
|
||||||
|
|
||||||
get isAvailable() {
|
get isAvailable() {
|
||||||
const unAvailable = ['Resizing', 'Not Ready'];
|
const unAvailable = ['Resizing', 'Not Ready'];
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,20 @@ 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 {
|
||||||
|
get _availableActions() {
|
||||||
|
let out = super._availableActions;
|
||||||
|
|
||||||
|
out = out.map((action) => {
|
||||||
|
if (['download'].includes(action.action)) {
|
||||||
|
return { ...action, enabled: !!this.linkFor('update') };
|
||||||
|
}
|
||||||
|
|
||||||
|
return action;
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// prevent harvester secret detail page be overridden.
|
// prevent harvester secret detail page be overridden.
|
||||||
// See isFullPageOverride in https://github.com/rancher/dashboard/blob/master/shell/components/ResourceDetail/index.vue
|
// See isFullPageOverride in https://github.com/rancher/dashboard/blob/master/shell/components/ResourceDetail/index.vue
|
||||||
get fullDetailPageOverride() {
|
get fullDetailPageOverride() {
|
||||||
|
|||||||
@ -96,6 +96,10 @@ export default class HciStorageClass extends StorageClass {
|
|||||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get expandOnlineEncryptedVolumeFeatureEnabled() {
|
||||||
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('expandOnlineEncryptedVolume');
|
||||||
|
}
|
||||||
|
|
||||||
get thirdPartyStorageFeatureEnabled() {
|
get thirdPartyStorageFeatureEnabled() {
|
||||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
||||||
}
|
}
|
||||||
@ -106,6 +110,15 @@ export default class HciStorageClass extends StorageClass {
|
|||||||
|
|
||||||
get availableActions() {
|
get availableActions() {
|
||||||
let out = super.availableActions || [];
|
let out = super.availableActions || [];
|
||||||
|
const canUpdate = !!this.linkFor('update');
|
||||||
|
|
||||||
|
out = out.map((action) => {
|
||||||
|
if (['setDefault', 'setAsDefault', 'resetDefault'].includes(action.action)) {
|
||||||
|
return { ...action, enabled: canUpdate };
|
||||||
|
}
|
||||||
|
|
||||||
|
return action;
|
||||||
|
});
|
||||||
|
|
||||||
if (this.isInternalStorageClass()) {
|
if (this.isInternalStorageClass()) {
|
||||||
out = out.filter((action) => {
|
out = out.filter((action) => {
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import { HCI as HCI_ANNOTATIONS } from '../config/labels-annotations';
|
|||||||
import HarvesterResource from './harvester';
|
import HarvesterResource from './harvester';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
|
|
||||||
|
const HARVESTER_NVIDIA_DRIVER_TOOLKIT = 'harvester-system/nvidia-driver-toolkit';
|
||||||
|
|
||||||
export default class HciAddonConfig extends HarvesterResource {
|
export default class HciAddonConfig extends HarvesterResource {
|
||||||
get availableActions() {
|
get availableActions() {
|
||||||
const out = super._availableActions;
|
const out = super._availableActions;
|
||||||
@ -19,9 +21,10 @@ export default class HciAddonConfig extends HarvesterResource {
|
|||||||
out.push(rancherDashboard);
|
out.push(rancherDashboard);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canUpdate = !!this.linkFor('update');
|
||||||
const toggleAddon = {
|
const toggleAddon = {
|
||||||
action: 'toggleAddon',
|
action: 'toggleAddon',
|
||||||
enabled: true,
|
enabled: canUpdate,
|
||||||
icon: this.spec.enabled ? 'icon icon-pause' : 'icon icon-play',
|
icon: this.spec.enabled ? 'icon icon-pause' : 'icon icon-play',
|
||||||
label: this.spec.enabled ? this.t('generic.disable') : this.t('generic.enable'),
|
label: this.spec.enabled ? this.t('generic.disable') : this.t('generic.enable'),
|
||||||
};
|
};
|
||||||
@ -45,6 +48,15 @@ export default class HciAddonConfig extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.spec.enabled && this.id === HARVESTER_NVIDIA_DRIVER_TOOLKIT) {
|
||||||
|
this.$dispatch('promptModal', {
|
||||||
|
resources: [this],
|
||||||
|
component: 'HarvesterEnableNvidiaDriverToolkit',
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.spec.enabled = !this.spec.enabled;
|
this.spec.enabled = !this.spec.enabled;
|
||||||
await this.save();
|
await this.save();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -3,6 +3,20 @@ import { findBy } from '@shell/utils/array';
|
|||||||
import HarvesterResource from './harvester';
|
import HarvesterResource from './harvester';
|
||||||
|
|
||||||
export default class HciKeypair extends HarvesterResource {
|
export default class HciKeypair extends HarvesterResource {
|
||||||
|
get _availableActions() {
|
||||||
|
let out = super._availableActions;
|
||||||
|
|
||||||
|
out = out.map((action) => {
|
||||||
|
if (['download'].includes(action.action)) {
|
||||||
|
return { ...action, enabled: !!this.linkFor('update') };
|
||||||
|
}
|
||||||
|
|
||||||
|
return action;
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
get stateDisplay() {
|
get stateDisplay() {
|
||||||
const conditions = get(this, 'status.conditions');
|
const conditions = get(this, 'status.conditions');
|
||||||
const status = (findBy(conditions, 'type', 'validated') || {}).status ;
|
const status = (findBy(conditions, 'type', 'validated') || {}).status ;
|
||||||
|
|||||||
@ -19,16 +19,18 @@ export default class ScheduleVmBackup extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const canUpdate = !!this.linkFor('update');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
action: 'resumeSchedule',
|
action: 'resumeSchedule',
|
||||||
enabled: ucFirst(this.state) === STATES.suspended.label,
|
enabled: canUpdate && ucFirst(this.state) === STATES.suspended.label,
|
||||||
icon: 'icons icon-play',
|
icon: 'icons icon-play',
|
||||||
label: this.t('harvester.action.resumeSchedule'),
|
label: this.t('harvester.action.resumeSchedule'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'suspendSchedule',
|
action: 'suspendSchedule',
|
||||||
enabled: ucFirst(this.state) === STATES.active.label,
|
enabled: canUpdate && ucFirst(this.state) === STATES.active.label,
|
||||||
icon: 'icons icon-pause',
|
icon: 'icons icon-pause',
|
||||||
label: this.t('harvester.action.suspendSchedule'),
|
label: this.t('harvester.action.suspendSchedule'),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -39,9 +39,6 @@ function isReady() {
|
|||||||
export default class HciVmImage extends HarvesterResource {
|
export default class HciVmImage extends HarvesterResource {
|
||||||
get availableActions() {
|
get availableActions() {
|
||||||
let out = super._availableActions;
|
let out = super._availableActions;
|
||||||
const toFilter = ['goToEditYaml'];
|
|
||||||
|
|
||||||
out = out.filter( (A) => !toFilter.includes(A.action));
|
|
||||||
|
|
||||||
// show `Clone` only when imageSource is `download`
|
// show `Clone` only when imageSource is `download`
|
||||||
if (this.imageSource !== 'download') {
|
if (this.imageSource !== 'download') {
|
||||||
@ -55,6 +52,7 @@ export default class HciVmImage extends HarvesterResource {
|
|||||||
canCreateVM = false;
|
canCreateVM = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canCreateImage = !!this.$getters?.['schemaFor']?.(HCI.IMAGE)?.collectionMethods?.some((method) => method.toLowerCase() === 'post');
|
||||||
const customActions = this.isReady ? [
|
const customActions = this.isReady ? [
|
||||||
{
|
{
|
||||||
action: 'createFromImage',
|
action: 'createFromImage',
|
||||||
@ -64,13 +62,13 @@ export default class HciVmImage extends HarvesterResource {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'encryptImage',
|
action: 'encryptImage',
|
||||||
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted,
|
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted && canCreateImage,
|
||||||
icon: 'icon icon-lock',
|
icon: 'icon icon-lock',
|
||||||
label: this.t('harvester.action.encryptImage'),
|
label: this.t('harvester.action.encryptImage'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'decryptImage',
|
action: 'decryptImage',
|
||||||
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted,
|
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted && canCreateImage,
|
||||||
icon: 'icon icon-unlock',
|
icon: 'icon icon-unlock',
|
||||||
label: this.t('harvester.action.decryptImage'),
|
label: this.t('harvester.action.decryptImage'),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -23,17 +23,17 @@ export default class HciVmTemplateVersion extends HarvesterResource {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const schema = this.$getters['schemaFor'](HCI.VM);
|
const schema = this.$getters['schemaFor'](HCI.VM);
|
||||||
let canCreateVM = true;
|
let canCreateVM = false;
|
||||||
|
|
||||||
if ( schema && !schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())) ) {
|
if (schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())) ) {
|
||||||
canCreateVM = false;
|
canCreateVM = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
action: 'launchFromTemplate',
|
action: 'launchFromTemplate',
|
||||||
icon: 'icon icon-spinner',
|
icon: 'icon icon-spinner',
|
||||||
disabled: !canCreateVM || !this.isReady,
|
enabled: canCreateVM && this.isReady,
|
||||||
label: this.t('harvester.action.launchFormTemplate'),
|
label: this.t('harvester.action.launchFormTemplate'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -83,17 +83,60 @@ const VMIPhase = {
|
|||||||
|
|
||||||
let productInStore;
|
let productInStore;
|
||||||
|
|
||||||
|
let _podOwnerMap = null;
|
||||||
|
let _podOwnerMapSource = null;
|
||||||
|
|
||||||
|
function getPodByOwnerName(rootGetters, inStore, ownerName) {
|
||||||
|
const podList = rootGetters[`${ inStore }/all`](POD);
|
||||||
|
|
||||||
|
if (!Array.isArray(podList)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
// if not equals (usually means the pod list has been updated), we need to rebuild the map, otherwise we can reuse the map for better performance
|
||||||
|
if (_podOwnerMapSource !== podList) {
|
||||||
|
_podOwnerMap = new Map(); // use Map to store ownerReference name and pod mapping
|
||||||
|
for (const pod of podList) {
|
||||||
|
const refName = pod.metadata?.ownerReferences?.[0]?.name;
|
||||||
|
|
||||||
|
if (refName) {
|
||||||
|
_podOwnerMap.set(refName, pod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_podOwnerMapSource = podList;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _podOwnerMap.get(ownerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPvcsByNames(rootGetters, inStore, names) {
|
||||||
|
const pvcList = rootGetters[`${ inStore }/all`](PVC);
|
||||||
|
|
||||||
|
if (!Array.isArray(pvcList)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const uniqueNames = new Set(names);
|
||||||
|
|
||||||
|
return pvcList.filter((pvc) => uniqueNames.has(pvc.metadata?.name));
|
||||||
|
}
|
||||||
|
|
||||||
const IgnoreMessages = ['pod has unbound immediate PersistentVolumeClaims'];
|
const IgnoreMessages = ['pod has unbound immediate PersistentVolumeClaims'];
|
||||||
|
|
||||||
export default class VirtVm extends HarvesterResource {
|
export default class VirtVm extends HarvesterResource {
|
||||||
get availableActions() {
|
get availableActions() {
|
||||||
const out = super._availableActions;
|
let out = super._availableActions;
|
||||||
|
|
||||||
|
if (this.isCloneBackendStorageCloning || this.isCloneBackendStorageFailed) {
|
||||||
|
out = out.filter(({ action }) => action !== 'goToClone');
|
||||||
|
}
|
||||||
|
|
||||||
const clone = out.find((action) => action.action === 'goToClone');
|
const clone = out.find((action) => action.action === 'goToClone');
|
||||||
|
|
||||||
if (clone) {
|
if (clone) {
|
||||||
clone.action = 'goToCloneVM';
|
clone.action = 'goToCloneVM';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canCreateVMSSchedule = !!this.$getters?.['schemaFor']?.(HCI.SCHEDULE_VM_BACKUP)?.collectionMethods?.find((x) => ['post'].includes(x.toLowerCase()));
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
action: 'stopVM',
|
action: 'stopVM',
|
||||||
@ -126,6 +169,7 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'restartVM',
|
action: 'restartVM',
|
||||||
|
altAction: 'altRestartVM',
|
||||||
enabled: !!this.actions?.restart,
|
enabled: !!this.actions?.restart,
|
||||||
icon: 'icon icon-refresh',
|
icon: 'icon icon-refresh',
|
||||||
label: this.t('harvester.action.restart'),
|
label: this.t('harvester.action.restart'),
|
||||||
@ -134,6 +178,7 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'softrebootVM',
|
action: 'softrebootVM',
|
||||||
|
altAction: 'doSoftReboot',
|
||||||
enabled: !!this.actions?.softreboot,
|
enabled: !!this.actions?.softreboot,
|
||||||
icon: 'icon icon-pipeline',
|
icon: 'icon icon-pipeline',
|
||||||
label: this.t('harvester.action.softreboot')
|
label: this.t('harvester.action.softreboot')
|
||||||
@ -143,7 +188,8 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
enabled: !!this.actions?.start,
|
enabled: !!this.actions?.start,
|
||||||
icon: 'icon icon-play',
|
icon: 'icon icon-play',
|
||||||
label: this.t('harvester.action.start'),
|
label: this.t('harvester.action.start'),
|
||||||
bulkable: true
|
bulkable: true,
|
||||||
|
bulkAction: 'startVM'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'backupVM',
|
action: 'backupVM',
|
||||||
@ -171,7 +217,7 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'createSchedule',
|
action: 'createSchedule',
|
||||||
enabled: this.schedulingVMBackupFeatureEnabled,
|
enabled: canCreateVMSSchedule && this.schedulingVMBackupFeatureEnabled,
|
||||||
icon: 'icon icon-history',
|
icon: 'icon icon-history',
|
||||||
label: this.t('harvester.action.createSchedule')
|
label: this.t('harvester.action.createSchedule')
|
||||||
},
|
},
|
||||||
@ -191,7 +237,9 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
action: 'migrateVM',
|
action: 'migrateVM',
|
||||||
enabled: !!this.actions?.migrate,
|
enabled: !!this.actions?.migrate,
|
||||||
icon: 'icon icon-copy',
|
icon: 'icon icon-copy',
|
||||||
label: this.t('harvester.action.migrate')
|
label: this.t('harvester.action.vmMigrate'),
|
||||||
|
bulkable: true,
|
||||||
|
bulkAction: 'migrateVM'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'abortMigrationVM',
|
action: 'abortMigrationVM',
|
||||||
@ -502,16 +550,38 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
altStopVM() {
|
async altRestartVM() {
|
||||||
this.doActionGrowl('stop', {});
|
await this.doActionGrowl('restart', {});
|
||||||
|
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
forceStop() {
|
async altStopVM() {
|
||||||
this.doActionGrowl('forceStop', {});
|
await this.doActionGrowl('stop', {});
|
||||||
|
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
startVM() {
|
async forceStop() {
|
||||||
this.doActionGrowl('start', {});
|
await this.doActionGrowl('forceStop', {});
|
||||||
|
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async startVM(resources = this) {
|
||||||
|
const list = Array.isArray(resources) ? resources : [resources];
|
||||||
|
|
||||||
|
for (const r of list) {
|
||||||
|
await r.doActionGrowl('start', {});
|
||||||
|
}
|
||||||
|
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async download() {
|
||||||
|
await super.download();
|
||||||
|
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadBulk(items) {
|
||||||
|
await super.downloadBulk(items);
|
||||||
|
this.$dispatch('promptModal', { performCallback: true, clearTableSelection: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
migrateVM(resources = this) {
|
migrateVM(resources = this) {
|
||||||
@ -660,16 +730,13 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
|
|
||||||
get podResource() {
|
get podResource() {
|
||||||
const inStore = this.productInStore;
|
const inStore = this.productInStore;
|
||||||
|
|
||||||
const vmiResource = this.$rootGetters[`${ inStore }/byId`](HCI.VMI, this.id);
|
const vmiResource = this.$rootGetters[`${ inStore }/byId`](HCI.VMI, this.id);
|
||||||
const podList = this.$rootGetters[`${ inStore }/all`](POD);
|
|
||||||
|
|
||||||
return podList.find((P) => {
|
if (!vmiResource?.metadata?.name) {
|
||||||
return (
|
return undefined;
|
||||||
vmiResource?.metadata?.name &&
|
}
|
||||||
vmiResource?.metadata?.name === P.metadata?.ownerReferences?.[0].name
|
|
||||||
);
|
return getPodByOwnerName(this.$rootGetters, inStore, vmiResource.metadata.name);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get isPaused() {
|
get isPaused() {
|
||||||
@ -710,17 +777,13 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
get vmi() {
|
get vmi() {
|
||||||
const inStore = this.productInStore;
|
const inStore = this.productInStore;
|
||||||
|
|
||||||
const vmis = this.$rootGetters[`${ inStore }/all`](HCI.VMI);
|
return this.$rootGetters[`${ inStore }/byId`](HCI.VMI, this.id);
|
||||||
|
|
||||||
return vmis.find((VMI) => VMI.id === this.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get volumes() {
|
get volumes() {
|
||||||
const pvcs = this.$rootGetters[`${ this.productInStore }/all`](PVC);
|
|
||||||
|
|
||||||
const volumeClaimNames = this.spec.template.spec.volumes?.map((v) => v.persistentVolumeClaim?.claimName).filter((v) => !!v) || [];
|
const volumeClaimNames = this.spec.template.spec.volumes?.map((v) => v.persistentVolumeClaim?.claimName).filter((v) => !!v) || [];
|
||||||
|
|
||||||
return pvcs.filter((pvc) => volumeClaimNames.includes(pvc.metadata.name));
|
return getPvcsByNames(this.$rootGetters, this.productInStore, volumeClaimNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
get lvmVolumes() {
|
get lvmVolumes() {
|
||||||
@ -731,6 +794,18 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
return this.volumes.filter((volume) => volume?.isLonghornV2);
|
return this.volumes.filter((volume) => volume?.isLonghornV2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get cloneBackendStorageStatus() {
|
||||||
|
return this.metadata?.annotations?.[HCI_ANNOTATIONS.CLONE_BACKEND_STORAGE_STATUS]?.toLowerCase() || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
get isCloneBackendStorageCloning() {
|
||||||
|
return this.cloneBackendStorageStatus === 'cloning';
|
||||||
|
}
|
||||||
|
|
||||||
|
get isCloneBackendStorageFailed() {
|
||||||
|
return this.cloneBackendStorageStatus === 'failed';
|
||||||
|
}
|
||||||
|
|
||||||
get encryptedVolumeType() {
|
get encryptedVolumeType() {
|
||||||
if (!this.volumes || this.volumes.length === 0) {
|
if (!this.volumes || this.volumes.length === 0) {
|
||||||
return 'none';
|
return 'none';
|
||||||
@ -753,17 +828,6 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
return { status: 'VMI error', detailedMessage: vmiFailureCond.message };
|
return { status: 'VMI error', detailedMessage: vmiFailureCond.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((this.vmi || this.isVMCreated) && this.podResource) {
|
|
||||||
// const podStatus = this.podResource.getPodStatus;
|
|
||||||
// if (POD_STATUS_ALL_ERROR.includes(podStatus?.status)) {
|
|
||||||
// return {
|
|
||||||
// ...podStatus,
|
|
||||||
// status: 'LAUNCHER_POD_ERROR',
|
|
||||||
// pod: this.podResource,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
return this?.vmi?.status?.phase;
|
return this?.vmi?.status?.phase;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -797,13 +861,21 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
!this.isVMExpectedRunning &&
|
!this.isVMExpectedRunning &&
|
||||||
this.isVMCreated &&
|
this.isVMCreated &&
|
||||||
this.vmi?.status?.phase === VMIPhase.Pending
|
this.vmi?.status?.phase === VMIPhase.Pending
|
||||||
) || (this.metadata?.annotations?.[HCI_ANNOTATIONS.CLONE_BACKEND_STORAGE_STATUS] === 'cloning')) {
|
) || this.isCloneBackendStorageCloning) {
|
||||||
return { status: VMIPhase.Pending };
|
return { status: VMIPhase.Pending };
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isCloneFailed() {
|
||||||
|
if (this.isCloneBackendStorageFailed) {
|
||||||
|
return { status: VMIPhase.Failed };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
get isStopping() {
|
get isStopping() {
|
||||||
if (this &&
|
if (this &&
|
||||||
!this.isVMExpectedRunning &&
|
!this.isVMExpectedRunning &&
|
||||||
@ -901,9 +973,7 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
|
|
||||||
const inStore = this.productInStore;
|
const inStore = this.productInStore;
|
||||||
|
|
||||||
const allRestore = this.$rootGetters[`${ inStore }/all`](HCI.RESTORE);
|
const res = this.$rootGetters[`${ inStore }/byId`](HCI.RESTORE, id);
|
||||||
|
|
||||||
const res = allRestore.find((O) => O.id === id);
|
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
const allBackups = this.$rootGetters[`${ inStore }/all`](HCI.BACKUP);
|
const allBackups = this.$rootGetters[`${ inStore }/all`](HCI.BACKUP);
|
||||||
@ -962,6 +1032,7 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
this.isUnschedulable?.status ||
|
this.isUnschedulable?.status ||
|
||||||
this.isPaused?.status ||
|
this.isPaused?.status ||
|
||||||
this.isVMError?.status ||
|
this.isVMError?.status ||
|
||||||
|
this.isCloneFailed?.status ||
|
||||||
this.isPending?.status ||
|
this.isPending?.status ||
|
||||||
this.isStopping?.status ||
|
this.isStopping?.status ||
|
||||||
this.isOff?.status ||
|
this.isOff?.status ||
|
||||||
@ -969,7 +1040,7 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
this.isRunning?.status ||
|
this.isRunning?.status ||
|
||||||
this.isNotReady?.status ||
|
this.isNotReady?.status ||
|
||||||
this.isStarting?.status ||
|
this.isStarting?.status ||
|
||||||
this.isWaitingForVMI?.state ||
|
this.isWaitingForVMI?.status ||
|
||||||
this.otherState?.status;
|
this.otherState?.status;
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
@ -1073,42 +1144,6 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
get warningCount() {
|
|
||||||
return this.resourcesStatus.warningCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
get errorCount() {
|
|
||||||
return this.resourcesStatus.errorCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
get resourcesStatus() {
|
|
||||||
const inStore = this.productInStore;
|
|
||||||
const vmList = this.$rootGetters[`${ inStore }/all`](HCI.VM);
|
|
||||||
let warningCount = 0;
|
|
||||||
let errorCount = 0;
|
|
||||||
|
|
||||||
vmList.forEach((vm) => {
|
|
||||||
const status = vm.actualState;
|
|
||||||
|
|
||||||
if (status === VM_ERROR) {
|
|
||||||
errorCount += 1;
|
|
||||||
} else if (
|
|
||||||
status === 'Stopping' ||
|
|
||||||
status === 'Waiting' ||
|
|
||||||
status === 'Pending' ||
|
|
||||||
status === 'Starting' ||
|
|
||||||
status === 'Terminating'
|
|
||||||
) {
|
|
||||||
warningCount += 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
warningCount,
|
|
||||||
errorCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
get volumeClaimTemplates() {
|
get volumeClaimTemplates() {
|
||||||
return parseVolumeClaimTemplates(this);
|
return parseVolumeClaimTemplates(this);
|
||||||
}
|
}
|
||||||
@ -1126,7 +1161,6 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
get rootImageId() {
|
get rootImageId() {
|
||||||
let imageId = '';
|
let imageId = '';
|
||||||
const inStore = this.productInStore;
|
const inStore = this.productInStore;
|
||||||
const pvcs = this.$rootGetters[`${ inStore }/all`](PVC) || [];
|
|
||||||
|
|
||||||
const volumes = this.spec.template.spec.volumes || [];
|
const volumes = this.spec.template.spec.volumes || [];
|
||||||
|
|
||||||
@ -1136,9 +1170,7 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!isNoExistingVolume) {
|
if (!isNoExistingVolume) {
|
||||||
const existingVolume = pvcs.find(
|
const existingVolume = this.$rootGetters[`${ inStore }/byId`](PVC, `${ this.metadata.namespace }/${ firstVolumeName }`);
|
||||||
(P) => P.id === `${ this.metadata.namespace }/${ firstVolumeName }`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingVolume) {
|
if (existingVolume) {
|
||||||
return existingVolume?.metadata?.annotations?.[
|
return existingVolume?.metadata?.annotations?.[
|
||||||
@ -1287,6 +1319,10 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('schedulingVMBackup');
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('schedulingVMBackup');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get nameDisplay() {
|
||||||
|
return this.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME] || this.metadata?.name || this.id;
|
||||||
|
}
|
||||||
|
|
||||||
get volumeEncryptionFeatureEnabled() {
|
get volumeEncryptionFeatureEnabled() {
|
||||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('volumeEncryption');
|
||||||
}
|
}
|
||||||
@ -1316,8 +1352,7 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isBackupTargetUnavailable() {
|
get isBackupTargetUnavailable() {
|
||||||
const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || [];
|
const backupTargetSetting = this.$rootGetters['harvester/byId'](HCI.SETTING, 'backup-target');
|
||||||
const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target');
|
|
||||||
|
|
||||||
return isBackupTargetSettingUnavailable(backupTargetSetting);
|
return isBackupTargetSettingUnavailable(backupTargetSetting);
|
||||||
}
|
}
|
||||||
|
|||||||
19
pkg/harvester/models/management.cattle.io.project.js
Normal file
19
pkg/harvester/models/management.cattle.io.project.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
import shellProject from '@shell/models/management.cattle.io.project';
|
||||||
|
|
||||||
|
// This model controls `Project / Namespace` page in rancher integration mode
|
||||||
|
// Extend management.cattle.io.project model from shell
|
||||||
|
export default class Project extends shellProject {
|
||||||
|
get _availableActions() {
|
||||||
|
const canUpdate = !!this.linkFor('update');
|
||||||
|
|
||||||
|
// disable `Edit Config` action if user does not have update permission.
|
||||||
|
return super._availableActions.map((action) => {
|
||||||
|
if (action.action === 'goToEdit') {
|
||||||
|
return { ...action, enabled: canUpdate };
|
||||||
|
}
|
||||||
|
|
||||||
|
return action;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -57,7 +57,11 @@ export default class HciVlanConfig extends HarvesterResource {
|
|||||||
get _availableActions() {
|
get _availableActions() {
|
||||||
const out = super._availableActions;
|
const out = super._availableActions;
|
||||||
|
|
||||||
|
const canMigrate = !!this.$getters?.['schemaFor']?.(HCI.VLAN_CONFIG)?.collectionMethods?.find((x) => ['post'].includes(x.toLowerCase()));
|
||||||
|
|
||||||
|
if (canMigrate) {
|
||||||
insertAt(out, 0, this.migrateAction);
|
insertAt(out, 0, this.migrateAction);
|
||||||
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester",
|
"name": "harvester",
|
||||||
"description": "Rancher UI Extension for Harvester",
|
"description": "Rancher UI Extension for Harvester",
|
||||||
"version": "1.8.1-dev",
|
"version": "1.9.0-dev",
|
||||||
"private": false,
|
"private": false,
|
||||||
"rancher": {
|
"rancher": {
|
||||||
"annotations": {
|
"annotations": {
|
||||||
|
|||||||
@ -123,6 +123,7 @@ export default {
|
|||||||
this.value?.[0]?.currentRouter().push(goTo);
|
this.value?.[0]?.currentRouter().push(goTo);
|
||||||
}
|
}
|
||||||
this.close();
|
this.close();
|
||||||
|
this.$store.commit('action-menu/togglePromptModal', { performCallback: true, clearTableSelection: true });
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
this.$emit('errors', err);
|
this.$emit('errors', err);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,7 +3,8 @@ import { ClusterNotFoundError } from '@shell/utils/error';
|
|||||||
import { SETTING } from '@shell/config/settings';
|
import { SETTING } from '@shell/config/settings';
|
||||||
import { COUNT, NAMESPACE, MANAGEMENT } from '@shell/config/types';
|
import { COUNT, NAMESPACE, MANAGEMENT } from '@shell/config/types';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { DEV } from '@shell/store/prefs';
|
import { DEV, NAMESPACE_FILTERS } from '@shell/store/prefs';
|
||||||
|
import { createNamespaceFilterKeyWithId } from '@shell/utils/namespace-filter';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -121,8 +122,11 @@ export default {
|
|||||||
|
|
||||||
await dispatch('cleanNamespaces', null, { root: true });
|
await dispatch('cleanNamespaces', null, { root: true });
|
||||||
|
|
||||||
|
const namespaceFilterKey = createNamespaceFilterKeyWithId(id, 'harvester');
|
||||||
|
const savedFilters = rootGetters['prefs/get'](NAMESPACE_FILTERS)?.[namespaceFilterKey];
|
||||||
|
|
||||||
commit('updateNamespaces', {
|
commit('updateNamespaces', {
|
||||||
filters: [],
|
filters: savedFilters || [],
|
||||||
all: getters.filterNamespace(),
|
all: getters.filterNamespace(),
|
||||||
getters
|
getters
|
||||||
}, { root: true });
|
}, { root: true });
|
||||||
|
|||||||
@ -123,5 +123,26 @@ export default {
|
|||||||
const clusterId = currentCluster.id;
|
const clusterId = currentCluster.id;
|
||||||
|
|
||||||
return projectsInAllClusters.filter((project: any) => project.spec.clusterName === clusterId && project.nameDisplay !== 'System');
|
return projectsInAllClusters.filter((project: any) => project.spec.clusterName === clusterId && project.nameDisplay !== 'System');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Few harvester resources name and REAL resource are different. E.g. HCI.NETWORK_ATTACHMENT page resource is NETWORK_ATTACHMENT.
|
||||||
|
// Check in config/harvester-cluster.js for more details.
|
||||||
|
// We need to look up the schema by resource name, and fallback to find using real resource name
|
||||||
|
schemaFor: (state, getters, rootState, rootGetters) => (type, _fuzzy = false, _allowThrow = true) => {
|
||||||
|
// follow the same logic as type-map/schemaFor in /dashboard/shell/plugins/dashboard-store/getters.js
|
||||||
|
const normalizedType = getters.normalizeType(type);
|
||||||
|
const schemas = state.types['schema'];
|
||||||
|
const out = schemas?.map?.get(normalizedType);
|
||||||
|
|
||||||
|
if (out) return out;
|
||||||
|
|
||||||
|
// if not found, use the resource mapping in configureType for a second try
|
||||||
|
const resourceType = rootGetters['type-map/optionsFor'](type)?.resource;
|
||||||
|
if (resourceType && resourceType !== type) {
|
||||||
|
const normalizedResource = getters.normalizeType(resourceType);
|
||||||
|
return schemas?.map?.get(normalizedResource) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,9 @@
|
|||||||
|
// To find the CRD name, you can run `kubectl api-resources` and look for the `NAME` column.
|
||||||
|
// The CRD name is usually in the format of `<plural>.<group>`, where `<plural>` is the plural form of the resource and `<group>` is the API group it belongs to.
|
||||||
|
// e.g
|
||||||
|
// 1. `virtualmachines.kubevirt.io` -> kubevirt.io.virtualmachine, the CRD name for the `VirtualMachine` resource in the `kubevirt.io` API group
|
||||||
|
// 2. `vpc-nat-gateways.kubeovn.io` -> kubeovn.io.vpcnatgateway, the CRD name for the `VpcNatGateway` resource in the `kubeovn.io` API group.
|
||||||
|
|
||||||
export const HCI = {
|
export const HCI = {
|
||||||
VM: 'kubevirt.io.virtualmachine',
|
VM: 'kubevirt.io.virtualmachine',
|
||||||
VMI: 'kubevirt.io.virtualmachineinstance',
|
VMI: 'kubevirt.io.virtualmachineinstance',
|
||||||
@ -16,9 +22,16 @@ export const HCI = {
|
|||||||
RESTORE: 'harvesterhci.io.virtualmachinerestore',
|
RESTORE: 'harvesterhci.io.virtualmachinerestore',
|
||||||
NODE_NETWORK: 'network.harvesterhci.io.nodenetwork',
|
NODE_NETWORK: 'network.harvesterhci.io.nodenetwork',
|
||||||
CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork',
|
CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork',
|
||||||
|
HOST_NETWORK_CONFIG: 'network.harvesterhci.io.hostnetworkconfig',
|
||||||
SUBNET: 'kubeovn.io.subnet',
|
SUBNET: 'kubeovn.io.subnet',
|
||||||
VPC: 'kubeovn.io.vpc',
|
VPC: 'kubeovn.io.vpc',
|
||||||
IP: 'kubeovn.io.ip',
|
IP: 'kubeovn.io.ip',
|
||||||
|
VLAN: 'kubeovn.io.vlan',
|
||||||
|
IPTABLES_EIP: 'kubeovn.io.iptableseip',
|
||||||
|
IPTABLES_SNAT_RULE: 'kubeovn.io.iptablessnatrule',
|
||||||
|
IPTABLES_DNAT_RULE: 'kubeovn.io.iptablesdnatrule',
|
||||||
|
PROVIDER_NETWORK: 'kubeovn.io.providernetwork',
|
||||||
|
VPC_NAT_GATEWAY: 'kubeovn.io.vpcnatgateway',
|
||||||
VM_IMAGE_DOWNLOADER: 'harvesterhci.io.virtualmachineimagedownloader',
|
VM_IMAGE_DOWNLOADER: 'harvesterhci.io.virtualmachineimagedownloader',
|
||||||
SUPPORT_BUNDLE: 'harvesterhci.io.supportbundle',
|
SUPPORT_BUNDLE: 'harvesterhci.io.supportbundle',
|
||||||
NETWORK_ATTACHMENT: 'harvesterhci.io.networkattachmentdefinition',
|
NETWORK_ATTACHMENT: 'harvesterhci.io.networkattachmentdefinition',
|
||||||
@ -62,6 +75,7 @@ export const HCI = {
|
|||||||
VMIMPORT_SOURCE_OVA: 'migration.harvesterhci.io.ovasource',
|
VMIMPORT_SOURCE_OVA: 'migration.harvesterhci.io.ovasource',
|
||||||
VMIMPORT: 'migration.harvesterhci.io.virtualmachineimport',
|
VMIMPORT: 'migration.harvesterhci.io.virtualmachineimport',
|
||||||
MIGRATION: 'migration.harvesterhci.io',
|
MIGRATION: 'migration.harvesterhci.io',
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot';
|
export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot';
|
||||||
|
|||||||
@ -31,22 +31,42 @@ export function registerAddonSideNav(store, productName, {
|
|||||||
}, 600);
|
}, 600);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Adds or removes the resource IDs from the product visibility whitelist.
|
const hasAccessibleSchema = (t) => {
|
||||||
const setMenuVisibility = (visible) => {
|
try {
|
||||||
if (visible) {
|
return !!store.getters[`${ productName }/schemaFor`]?.(t);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showTypes = (visibleTypes) => {
|
||||||
store.commit('type-map/basicType', {
|
store.commit('type-map/basicType', {
|
||||||
product: productName,
|
product: productName,
|
||||||
group: navGroup,
|
group: navGroup,
|
||||||
types
|
types: visibleTypes
|
||||||
});
|
});
|
||||||
} else {
|
};
|
||||||
// Manually delete the keys from the state object to hide them.
|
|
||||||
|
const hideTypes = () => {
|
||||||
const basicTypes = store.state['type-map'].basicTypes[productName];
|
const basicTypes = store.state['type-map'].basicTypes[productName];
|
||||||
|
|
||||||
if (basicTypes) {
|
if (basicTypes) {
|
||||||
types.forEach((t) => delete basicTypes[t]);
|
types.forEach((t) => delete basicTypes[t]);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Adds or removes the resource IDs from the product visibility whitelist.
|
||||||
|
const setMenuVisibility = (visible) => {
|
||||||
|
const accessibleTypes = visible ? types.filter(hasAccessibleSchema) : [];
|
||||||
|
|
||||||
|
// Always clear first to remove any previously-registered types that are
|
||||||
|
// no longer accessible (e.g. partial permission changes like types=[A,B] where B is dropped).
|
||||||
|
hideTypes();
|
||||||
|
|
||||||
|
if (accessibleTypes.length > 0) {
|
||||||
|
showTypes(accessibleTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
kickSideNav();
|
kickSideNav();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
21
pkg/harvester/utils/error.js
Normal file
21
pkg/harvester/utils/error.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
const AUTH_ERROR_CODES = [401, 403, 404];
|
||||||
|
|
||||||
|
export function getLoginAwareErrors(err, message = '') {
|
||||||
|
const errors = Array.isArray(err) ? err : (err ? [err] : []);
|
||||||
|
|
||||||
|
if (!errors.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const generic = message;
|
||||||
|
|
||||||
|
if (errors.some((e) => AUTH_ERROR_CODES.includes(e?._status || e?.response?.status))) {
|
||||||
|
return [generic];
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgs = errors
|
||||||
|
.map((e) => (typeof e === 'string' ? e : (e?.message || e?._statusText || '')))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return msgs.length ? msgs : [generic];
|
||||||
|
}
|
||||||
@ -27,23 +27,6 @@ OUTPUT_DIR=dist/${DIR}-embedded
|
|||||||
echo "Building..."
|
echo "Building..."
|
||||||
COMMIT=${COMMIT} VERSION=${VERSION} OUTPUT_DIR=$OUTPUT_DIR ROUTER_BASE='/dashboard/' RESOURCE_BASE='/dashboard/' RANCHER_ENV=harvester yarn run build
|
COMMIT=${COMMIT} VERSION=${VERSION} OUTPUT_DIR=$OUTPUT_DIR ROUTER_BASE='/dashboard/' RESOURCE_BASE='/dashboard/' RANCHER_ENV=harvester yarn run build
|
||||||
|
|
||||||
if [ -v EMBED_PKG ]; then
|
|
||||||
echo "Build and embed plugin from: $EMBED_PKG"
|
|
||||||
PKG_FILE_NAME=${EMBED_PKG##*/}
|
|
||||||
echo PKG_FILE_NAME: $PKG_FILE_NAME
|
|
||||||
|
|
||||||
PKG_NAME="${PKG_FILE_NAME/.tar.gz/""}"
|
|
||||||
echo "Plugin name: '$PKG_NAME'"
|
|
||||||
|
|
||||||
# Fetch file, unpack and move to dist
|
|
||||||
curl $EMBED_PKG --output $PKG_FILE_NAME
|
|
||||||
OUTPUT_DIR_PKG=$OUTPUT_DIR/$PKG_NAME
|
|
||||||
mkdir -p $OUTPUT_DIR_PKG
|
|
||||||
tar xvfz $PKG_FILE_NAME -C $OUTPUT_DIR/$PKG_NAME
|
|
||||||
echo "Plugin contents that will be served from $PKG_NAME"
|
|
||||||
ls -alR $OUTPUT_DIR/$PKG_NAME
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Destroying..."
|
echo "Destroying..."
|
||||||
find $OUTPUT_DIR -name "index.html" -mindepth 2 -exec rm {} \;
|
find $OUTPUT_DIR -name "index.html" -mindepth 2 -exec rm {} \;
|
||||||
find $OUTPUT_DIR -type d -empty -depth -exec rmdir {} \;
|
find $OUTPUT_DIR -type d -empty -depth -exec rmdir {} \;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user