mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-03 20:56:06 +08:00
Compare commits
36
Commits
v1.7.0-rc2
...
v1.7.1-rc4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2afb04947d | ||
|
|
730c68bf14 | ||
|
|
20bee39a6c | ||
|
|
dbb199d7bb | ||
|
|
5488979448 | ||
|
|
9e588e90c2 | ||
|
|
9378277102 | ||
|
|
b5e78018a5 | ||
|
|
f411a0c0af | ||
|
|
cfa58985cf | ||
|
|
66a8f9d0e7 | ||
|
|
bf61c7dd7d | ||
|
|
56d97260c4 | ||
|
|
beabb34920 | ||
|
|
d2609157bd | ||
|
|
8fbe1943d8 | ||
|
|
ec6bc4d639 | ||
|
|
3824a14730 | ||
|
|
0fc8bece02 | ||
|
|
39764af627 | ||
|
|
bdc87bda0e | ||
|
|
e0dc77624b | ||
|
|
c3ba10bd22 | ||
|
|
5b7d54d0a3 | ||
|
|
99a216dfa0 | ||
|
|
e4c85f510e | ||
|
|
f391f018de | ||
|
|
4b2e92ea15 | ||
|
|
2f956d5946 | ||
|
|
5f8d556ea2 | ||
|
|
396ab48f1c | ||
|
|
6b8c079018 | ||
|
|
7f638e86c8 | ||
|
|
0c4955a766 | ||
|
|
3f4ff30275 | ||
|
|
8e0332a364 |
+6
-6
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.7.0-rc2",
|
"version": "1.7.1-rc4",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-class-static-block": "7.28.3",
|
"@babel/plugin-transform-class-static-block": "7.28.6",
|
||||||
"@rancher/shell": "3.0.8-rc.8",
|
"@rancher/shell": "3.0.8-rc.8",
|
||||||
"cache-loader": "^4.1.0",
|
"cache-loader": "^4.1.0",
|
||||||
"color": "4.2.3",
|
"color": "4.2.3",
|
||||||
@@ -24,12 +24,12 @@
|
|||||||
"glob": "7.2.3",
|
"glob": "7.2.3",
|
||||||
"glob-parent": "6.0.2",
|
"glob-parent": "6.0.2",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"@types/lodash": "4.17.20",
|
"@types/lodash": "4.17.23",
|
||||||
"merge": "2.1.1",
|
"merge": "2.1.1",
|
||||||
"node-forge": "1.3.1",
|
"node-forge": "1.3.3",
|
||||||
"nth-check": "2.1.1",
|
"nth-check": "2.1.1",
|
||||||
"qs": "6.14.0",
|
"qs": "6.14.1",
|
||||||
"roarr": "7.21.2",
|
"roarr": "7.21.4",
|
||||||
"semver": "7.7.3",
|
"semver": "7.7.3",
|
||||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
# harvester-ui-extension
|
||||||
|
|
||||||
|
The Harvester UI Extension is a Rancher extension that provides the user interface for [Harvester](https://harvesterhci.io) within the [Rancher Dashboard](https://github.com/rancher/dashboard).
|
||||||
|
|
||||||
|
> **Note:**
|
||||||
|
> This extension is available starting from **Rancher 2.10.0**. Ensure your Rancher version is **2.10.0 or later** to access Harvester integration.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
For detailed installation instructions, please refer to the [official Harvester documentation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210).
|
||||||
|
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
Ensure **Node.js v20 or later** is installed for development and debugging.
|
||||||
|
|
||||||
|
### Standalone Mode
|
||||||
|
|
||||||
|
Run the extension standalone with hot reload at `https://localhost:8005`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# Start the development server
|
||||||
|
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
||||||
|
|
||||||
|
# Example with specific server version
|
||||||
|
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.5.0 API=https://192.168.1.123 yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
You may also define environment variables in a `.env` file:
|
||||||
|
|
||||||
|
```env
|
||||||
|
RANCHER_ENV=harvester
|
||||||
|
VUE_APP_SERVER_VERSION=v1.5.0
|
||||||
|
API=https://192.168.1.123
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rancher Integration Mode
|
||||||
|
|
||||||
|
To run as a Rancher extension, follow the [Rancher UI Extension Guide](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
API=https://your-rancher-ip yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commit Message Guidelines
|
||||||
|
|
||||||
|
This project uses [commit-lint](https://commitlint.js.org/) with [Conventional Commits](https://www.conventionalcommits.org/) to ensure consistent and meaningful commit messages.
|
||||||
|
|
||||||
|
### Commit Message Format
|
||||||
|
|
||||||
|
All commit messages must follow the conventional commit format:
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>[optional scope]: <description>
|
||||||
|
|
||||||
|
[optional body]
|
||||||
|
|
||||||
|
[optional footer(s)]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supported Types
|
||||||
|
|
||||||
|
- **feat**: New features
|
||||||
|
- **fix**: Bug fixes
|
||||||
|
- **docs**: Documentation changes
|
||||||
|
- **style**: Code style changes (formatting, missing semicolons, etc.)
|
||||||
|
- **refactor**: Code refactoring
|
||||||
|
- **perf**: Performance improvements
|
||||||
|
- **test**: Adding or updating tests
|
||||||
|
- **build**: Build system or external dependencies
|
||||||
|
- **ci**: CI/CD changes
|
||||||
|
- **chore**: Other changes that don't modify src or test files
|
||||||
|
- **revert**: Reverts a previous commit
|
||||||
|
- **wip**: Work in progress
|
||||||
|
- **deps**: Dependency updates
|
||||||
|
- **security**: Security fixes
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Feature
|
||||||
|
git commit -m "feat: add new virtual machine creation wizard"
|
||||||
|
|
||||||
|
# Bug fix
|
||||||
|
git commit -m "fix: resolve memory leak in VM console"
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
git commit -m "docs: update installation instructions"
|
||||||
|
|
||||||
|
# Breaking change
|
||||||
|
git commit -m "feat!: change API endpoint structure
|
||||||
|
|
||||||
|
BREAKING CHANGE: The /api/v1/vms endpoint has been replaced with /api/v2/vms"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Git Hooks
|
||||||
|
|
||||||
|
The project uses [Husky](https://typicode.github.io/husky/) to automatically validate commit messages and run linting before commits:
|
||||||
|
|
||||||
|
- **pre-commit**: Runs ESLint to ensure code quality
|
||||||
|
- **commit-msg**: Validates commit message format using commit-lint
|
||||||
|
|
||||||
|
These hooks are automatically installed when you run `yarn install`.
|
||||||
|
|
||||||
|
### Manual Validation
|
||||||
|
|
||||||
|
You can manually validate commit messages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Validate the last commit
|
||||||
|
yarn commitlint
|
||||||
|
|
||||||
|
# Validate a specific commit
|
||||||
|
npx commitlint --from <commit-hash>
|
||||||
|
|
||||||
|
# Validate a range of commits
|
||||||
|
npx commitlint --from <start-hash> --to <end-hash>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Branch Structure
|
||||||
|
|
||||||
|
- **`main`** – Main development branch
|
||||||
|
- **`release-harvester-vX.Y`** – Stable release branches per version series
|
||||||
|
- **`vX.Y-head`** – Testing branches for ongoing changes to extension builds in each release series
|
||||||
|
|
||||||
|
> **Note:**
|
||||||
|
> The `vX.Y-head` branches are auto-generated and kept in sync with release branches. Use these for testing the latest changes in each version series.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
|
||||||
|
### UI Extension Testing
|
||||||
|
|
||||||
|
To validate changes in a release series, switch to the appropriate `vX.Y-head` branch. For main branch testing, use `main-head`.
|
||||||
|
|
||||||
|
- Examples:
|
||||||
|
- Test `1.0.x` series → `v1.0-head`
|
||||||
|
- Test `1.5.x` series → `v1.5-head`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
|
||||||
|
2. Refresh the Harvester repository using the target `vX.Y-head` branch
|
||||||
|
3. Go to the **Extensions** page and install the desired version
|
||||||
|
|
||||||
|
### Standalone Mode Testing
|
||||||
|
|
||||||
|
To test the standalone UI, configure Harvester to load the UI from an external source.
|
||||||
|
|
||||||
|
- Examples of `ui-index`:
|
||||||
|
- Main branch → `https://releases.rancher.com/harvester-ui/dashboard/latest/index.html`
|
||||||
|
- Release series `1.5.x` → `https://releases.rancher.com/harvester-ui/dashboard/release-harvester-v1.5/index.html`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Go to **Harvester UI** → **Advanced** → **Settings** → **UI**
|
||||||
|
2. Set **ui-source** to `External`
|
||||||
|
3. Set **ui-index** to the desired URL
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
If you want to contribute, start by reading this document, then visit our [Getting Started guide](https://extensions.rancher.io/extensions/next/extensions-getting-started) to learn how to develop and submit changes.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright (c) 2014-2025 [SUSE, LLC.](https://www.suse.com/)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -18,16 +18,20 @@ export default {
|
|||||||
|
|
||||||
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
||||||
|
|
||||||
try {
|
if (this.customSupportBundleFeatureEnabled) {
|
||||||
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
try {
|
||||||
const response = await this.$store.dispatch('harvester/request', { url });
|
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
||||||
|
const response = await this.$store.dispatch('harvester/request', { url });
|
||||||
|
|
||||||
this.defaultNamespaces = response.data || [];
|
this.defaultNamespaces = response.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
this.defaultNamespaces = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
this.defaultNamespaces = [];
|
this.defaultNamespaces = [];
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -42,24 +46,38 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
customSupportBundleFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
|
||||||
|
},
|
||||||
|
|
||||||
allNamespaces() {
|
allNamespaces() {
|
||||||
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
||||||
},
|
},
|
||||||
|
|
||||||
filteredNamespaces() {
|
filteredNamespaces() {
|
||||||
|
if (!this.customSupportBundleFeatureEnabled) {
|
||||||
|
return this.allNamespaces;
|
||||||
|
}
|
||||||
|
|
||||||
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
|
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
|
||||||
|
|
||||||
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
|
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
|
||||||
},
|
},
|
||||||
|
|
||||||
namespaceOptions() {
|
namespaceOptions() {
|
||||||
|
const mappedNamespaces = this.filteredNamespaces.map((ns) => ({ label: ns, value: ns }));
|
||||||
|
|
||||||
|
if (!this.customSupportBundleFeatureEnabled) {
|
||||||
|
return mappedNamespaces;
|
||||||
|
}
|
||||||
|
|
||||||
const allSelected =
|
const allSelected =
|
||||||
this.namespaces.length === this.filteredNamespaces.length &&
|
this.namespaces.length === this.filteredNamespaces.length &&
|
||||||
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
|
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
|
||||||
|
|
||||||
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
|
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
|
||||||
|
|
||||||
return [controlOption, ...this.filteredNamespaces];
|
return [controlOption, ...mappedNamespaces];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ const FEATURE_FLAGS = {
|
|||||||
'l2VlanTrunkMode',
|
'l2VlanTrunkMode',
|
||||||
'kubevirtMigration',
|
'kubevirtMigration',
|
||||||
'hotplugNic'
|
'hotplugNic'
|
||||||
]
|
],
|
||||||
|
'v1.7.1': [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateFeatureFlags = () => {
|
const generateFeatureFlags = () => {
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export const HCI = {
|
|||||||
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
|
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
|
||||||
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
|
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
|
||||||
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
|
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
|
||||||
|
ADDON_DISPLAYNAME: 'addon.harvesterhci.io/displayName',
|
||||||
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
|
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
|
||||||
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
|
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
|
||||||
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
|
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
|
||||||
@@ -76,4 +77,5 @@ export const HCI = {
|
|||||||
CLONE_STRATEGY: 'cdi.harvesterhci.io/storageProfileCloneStrategy',
|
CLONE_STRATEGY: 'cdi.harvesterhci.io/storageProfileCloneStrategy',
|
||||||
VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes',
|
VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes',
|
||||||
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
|
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
|
||||||
|
MAC_ADDRESS: 'harvesterhci.io/mac-address',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default {
|
|||||||
resources: {
|
resources: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -43,7 +43,7 @@ export default {
|
|||||||
...mapState('action-menu', ['modalData']),
|
...mapState('action-menu', ['modalData']),
|
||||||
|
|
||||||
title() {
|
title() {
|
||||||
return this.modalData.title || 'dialog.promptRemove.title';
|
return this.modalData?.title || 'dialog.promptRemove.title';
|
||||||
},
|
},
|
||||||
|
|
||||||
formattedType() {
|
formattedType() {
|
||||||
@@ -51,7 +51,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
warningMessage() {
|
warningMessage() {
|
||||||
if (this.modalData.warningMessage) return this.modalData.warningMessage;
|
if (this.modalData?.warningMessage) return this.modalData.warningMessage;
|
||||||
|
|
||||||
const isPlural = this.type.endsWith('s');
|
const isPlural = this.type.endsWith('s');
|
||||||
const thisOrThese = isPlural ? 'these' : 'this';
|
const thisOrThese = isPlural ? 'these' : 'this';
|
||||||
@@ -145,6 +145,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
for (const resource of this.resources) {
|
for (const resource of this.resources) {
|
||||||
await resource.remove();
|
await resource.remove();
|
||||||
|
if (this.modalData?.extraActionAfterRemove) await this.modalData.extraActionAfterRemove();
|
||||||
}
|
}
|
||||||
buttonDone(true);
|
buttonDone(true);
|
||||||
this.close();
|
this.close();
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !pvc.attachVM;
|
return true;
|
||||||
})
|
})
|
||||||
.map((pvc) => {
|
.map((pvc) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -371,7 +371,7 @@ export default {
|
|||||||
<div class="key">
|
<div class="key">
|
||||||
<input
|
<input
|
||||||
v-model="scope.row.value"
|
v-model="scope.row.value"
|
||||||
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
:placeholder="t('harvester.subnet.excludeIPs.placeholder')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+6
-8
@@ -99,7 +99,6 @@ export default {
|
|||||||
this.allPVCs
|
this.allPVCs
|
||||||
.filter( (pvc) => {
|
.filter( (pvc) => {
|
||||||
let isAvailable = true;
|
let isAvailable = true;
|
||||||
let isBeingUsed = false;
|
|
||||||
|
|
||||||
this.rows.forEach( (O) => {
|
this.rows.forEach( (O) => {
|
||||||
if (O.volumeName === pvc.metadata.name) {
|
if (O.volumeName === pvc.metadata.name) {
|
||||||
@@ -111,17 +110,16 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// already used as image volume
|
||||||
|
if (this.idx > 0 && pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (pvc.isGoldenImageVolume) {
|
if (pvc.isGoldenImageVolume) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pvc.attachVM && isAvailable && pvc.attachVM?.id === this.vm?.id && this.isEdit) {
|
return isAvailable && pvc.isAvailable;
|
||||||
isBeingUsed = false;
|
|
||||||
} else if (pvc.attachVM) {
|
|
||||||
isBeingUsed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isAvailable && !isBeingUsed && pvc.isAvailable;
|
|
||||||
})
|
})
|
||||||
.map((pvc) => {
|
.map((pvc) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ export default {
|
|||||||
templateVersionId: '',
|
templateVersionId: '',
|
||||||
namePrefix: '',
|
namePrefix: '',
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
useTemplate: false,
|
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
hostname,
|
hostname,
|
||||||
isRestartImmediately,
|
isRestartImmediately,
|
||||||
@@ -255,6 +254,7 @@ export default {
|
|||||||
return volume;
|
return volume;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
delete cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.MAC_ADDRESS];
|
||||||
cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
||||||
|
|
||||||
// Update instance labels, labels and annotations
|
// Update instance labels, labels and annotations
|
||||||
@@ -490,6 +490,7 @@ export default {
|
|||||||
if (this.isSingle) {
|
if (this.isSingle) {
|
||||||
if (!this.value.spec.template.spec.hostname) {
|
if (!this.value.spec.template.spec.hostname) {
|
||||||
this.value.spec.template.spec['hostname'] = this.value.metadata.name;
|
this.value.spec.template.spec['hostname'] = this.value.metadata.name;
|
||||||
|
this.spec.template.spec['hostname'] = this.value.metadata.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -922,6 +922,8 @@ harvester:
|
|||||||
checksumTip: Validate the image using the SHA512 checksum, if specified.
|
checksumTip: Validate the image using the SHA512 checksum, if specified.
|
||||||
tooltip:
|
tooltip:
|
||||||
imported: Created automatically by the vm-import-controller
|
imported: Created automatically by the vm-import-controller
|
||||||
|
errors:
|
||||||
|
unsupportedBackend: 'Unsupported backend type: {backend}'
|
||||||
|
|
||||||
vmTemplate:
|
vmTemplate:
|
||||||
label: Templates
|
label: Templates
|
||||||
@@ -1089,6 +1091,8 @@ harvester:
|
|||||||
placeholder: e.g. 172.16.0.0/16
|
placeholder: e.g. 172.16.0.0/16
|
||||||
excludeIPs:
|
excludeIPs:
|
||||||
tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list.
|
tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list.
|
||||||
|
placeholder: single IP or 192.168.0.100..192.168.0.200
|
||||||
|
|
||||||
acl:
|
acl:
|
||||||
label: Access Control List
|
label: Access Control List
|
||||||
tooltip: The ACL to apply to this Subnet. Must be one of the ACLs in the same namespace.
|
tooltip: The ACL to apply to this Subnet. Must be one of the ACLs in the same namespace.
|
||||||
@@ -1204,7 +1208,7 @@ harvester:
|
|||||||
vlan: VLAN ID
|
vlan: VLAN ID
|
||||||
exclude:
|
exclude:
|
||||||
label: Exclude IPs
|
label: Exclude IPs
|
||||||
placeholder: e.g. 172.16.0.1
|
placeholder: CIDR format, e.g. 172.16.0.10/32
|
||||||
invalid: '"Exclude list" is invalid.'
|
invalid: '"Exclude list" is invalid.'
|
||||||
addIp: Add Exclude IP
|
addIp: Add Exclude IP
|
||||||
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
||||||
@@ -1559,6 +1563,7 @@ harvester:
|
|||||||
'harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
|
'harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
|
||||||
'harvester-system/harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
|
'harvester-system/harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
|
||||||
'harvester-csi-driver-lvm': harvester-csi-driver-lvm is an add-on allowing users to create PVC through the LVM with local devices.
|
'harvester-csi-driver-lvm': harvester-csi-driver-lvm is an add-on allowing users to create PVC through the LVM with local devices.
|
||||||
|
'descheduler': 'The virtual machine auto balance optimizes workload scheduling by evicting pods that are not optimally placed according to administrator-defined policies.'
|
||||||
vmImport:
|
vmImport:
|
||||||
titles:
|
titles:
|
||||||
basic: Basic
|
basic: Basic
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ export default {
|
|||||||
showYaml: false,
|
showYaml: false,
|
||||||
spec: null,
|
spec: null,
|
||||||
osType: 'linux',
|
osType: 'linux',
|
||||||
|
useTemplate: false,
|
||||||
sshKey: [],
|
sshKey: [],
|
||||||
maintenanceStrategies,
|
maintenanceStrategies,
|
||||||
maintenanceStrategy: 'Migrate',
|
maintenanceStrategy: 'Migrate',
|
||||||
@@ -273,7 +274,7 @@ export default {
|
|||||||
|
|
||||||
needNewSecret() {
|
needNewSecret() {
|
||||||
// When creating a template it is always necessary to create a new secret.
|
// When creating a template it is always necessary to create a new secret.
|
||||||
return this.isCreate ? true : this.showYaml ? false : this.resourceType === HCI.VM_VERSION;
|
return this.isCreate || this.showYaml ? false : this.resourceType === HCI.VM_VERSION;
|
||||||
},
|
},
|
||||||
|
|
||||||
defaultTerminationSetting() {
|
defaultTerminationSetting() {
|
||||||
@@ -689,7 +690,6 @@ export default {
|
|||||||
set(this.spec.template.spec, 'domain.memory.maxGuest', this.maxMemory);
|
set(this.spec.template.spec, 'domain.memory.maxGuest', this.maxMemory);
|
||||||
set(this.spec.template.spec, 'domain.resources.limits.memory', this.maxMemory);
|
set(this.spec.template.spec, 'domain.resources.limits.memory', this.maxMemory);
|
||||||
} else {
|
} else {
|
||||||
this.spec.template.spec.domain.cpu.maxSockets = 1;
|
|
||||||
this.spec.template.spec.domain.cpu.sockets = 1;
|
this.spec.template.spec.domain.cpu.sockets = 1;
|
||||||
this.spec.template.spec.domain.cpu.cores = this.cpu;
|
this.spec.template.spec.domain.cpu.cores = this.cpu;
|
||||||
this.spec.template.spec.domain.resources.limits.cpu = this.cpu?.toString();
|
this.spec.template.spec.domain.resources.limits.cpu = this.cpu?.toString();
|
||||||
@@ -709,16 +709,7 @@ export default {
|
|||||||
|
|
||||||
disk.forEach( (R, index) => {
|
disk.forEach( (R, index) => {
|
||||||
const prefixName = this.value.metadata?.name || '';
|
const prefixName = this.value.metadata?.name || '';
|
||||||
|
const dataVolumeName = this.parseDataVolumeName(R, prefixName);
|
||||||
let dataVolumeName = '';
|
|
||||||
|
|
||||||
if (R.source === SOURCE_TYPE.ATTACH_VOLUME) {
|
|
||||||
dataVolumeName = R.volumeName;
|
|
||||||
} else if (this.isClone || !this.hasCreateVolumes.includes(R.realName)) {
|
|
||||||
dataVolumeName = `${ prefixName }-${ R.name }-${ randomStr(5).toLowerCase() }`;
|
|
||||||
} else {
|
|
||||||
dataVolumeName = R.realName;
|
|
||||||
}
|
|
||||||
|
|
||||||
const _disk = this.parseDisk(R, index);
|
const _disk = this.parseDisk(R, index);
|
||||||
const _volume = this.parseVolume(R, dataVolumeName);
|
const _volume = this.parseVolume(R, dataVolumeName);
|
||||||
@@ -1013,6 +1004,20 @@ export default {
|
|||||||
this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
|
this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
parseDataVolumeName(R, prefixName) {
|
||||||
|
let dataVolumeName = '';
|
||||||
|
|
||||||
|
if (R.source === SOURCE_TYPE.ATTACH_VOLUME) {
|
||||||
|
dataVolumeName = R.volumeName;
|
||||||
|
} else if (this.isClone || !this.hasCreateVolumes.includes(R.realName)) {
|
||||||
|
dataVolumeName = `${ prefixName }-${ R.name }-${ randomStr(5).toLowerCase() }`;
|
||||||
|
} else {
|
||||||
|
dataVolumeName = R.realName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataVolumeName;
|
||||||
|
},
|
||||||
|
|
||||||
parseDisk(R, index) {
|
parseDisk(R, index) {
|
||||||
const out = { name: R.name };
|
const out = { name: R.name };
|
||||||
|
|
||||||
@@ -1638,7 +1643,8 @@ export default {
|
|||||||
|
|
||||||
secretRef: {
|
secretRef: {
|
||||||
handler(secret) {
|
handler(secret) {
|
||||||
if (secret && this.resourceType !== HCI.BACKUP) {
|
// we should not inherit the secret if it's from VM template.
|
||||||
|
if (secret && this.resourceType !== HCI.BACKUP && !this.useTemplate) {
|
||||||
this.secretName = secret?.metadata.name;
|
this.secretName = secret?.metadata.name;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import SteveModel from '@shell/plugins/steve/steve-class';
|
|
||||||
import { escapeHtml } from '@shell/utils/string';
|
import { escapeHtml } from '@shell/utils/string';
|
||||||
import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
|
import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
|
||||||
|
import HarvesterResource from './harvester';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class representing vGPU MIGConfiguration resource.
|
* Class representing vGPU MIGConfiguration resource.
|
||||||
* @extends SteveModal
|
* @extends HarvesterResource
|
||||||
*/
|
*/
|
||||||
export default class MIGCONFIGURATION extends SteveModel {
|
export default class MIGCONFIGURATION extends HarvesterResource {
|
||||||
get _availableActions() {
|
get _availableActions() {
|
||||||
let out = super._availableActions;
|
let out = super._availableActions;
|
||||||
|
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ export default class HciNode extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isCordoned() {
|
get isCordoned() {
|
||||||
return (this.isUnSchedulable && !this.isEtcd) || this.hasAction('uncordon');
|
return this.hasAction('uncordon');
|
||||||
}
|
}
|
||||||
|
|
||||||
get isEtcd() {
|
get isEtcd() {
|
||||||
|
|||||||
@@ -113,8 +113,9 @@ export default class HciAddonConfig extends HarvesterResource {
|
|||||||
|
|
||||||
get displayName() {
|
get displayName() {
|
||||||
const isExperimental = this.metadata?.labels?.[HCI_ANNOTATIONS.ADDON_EXPERIMENTAL] === 'true';
|
const isExperimental = this.metadata?.labels?.[HCI_ANNOTATIONS.ADDON_EXPERIMENTAL] === 'true';
|
||||||
|
const name = this.metadata?.labels?.[HCI_ANNOTATIONS.ADDON_DISPLAYNAME] || this.metadata.name;
|
||||||
|
|
||||||
return isExperimental ? `${ this.metadata.name } (${ this.t('generic.experimental') })` : this.metadata.name;
|
return isExperimental ? `${ name } (${ this.t('generic.experimental') })` : name;
|
||||||
}
|
}
|
||||||
|
|
||||||
get customValidationRules() {
|
get customValidationRules() {
|
||||||
|
|||||||
@@ -318,8 +318,21 @@ export default class HciVmImage extends HarvesterResource {
|
|||||||
get uploadImage() {
|
get uploadImage() {
|
||||||
return async(file, opt = {}) => {
|
return async(file, opt = {}) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
const backend = this.spec?.backend || 'backingimage';
|
||||||
|
const backendFieldMap = {
|
||||||
|
cdi: 'file',
|
||||||
|
backingimage: 'chunk'
|
||||||
|
};
|
||||||
|
const fieldName = backendFieldMap[backend];
|
||||||
|
|
||||||
formData.append('chunk', file);
|
if (!fieldName) {
|
||||||
|
const error = this.t('harvester.image.errors.unsupportedBackend', { backend });
|
||||||
|
|
||||||
|
this.$ctx.commit('harvester-common/uploadError', { name: this.name, message: error }, { root: true });
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.append(fieldName, file);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.$ctx.commit('harvester-common/uploadStart', this.metadata.name, { root: true });
|
this.$ctx.commit('harvester-common/uploadStart', this.metadata.name, { root: true });
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester",
|
"name": "harvester",
|
||||||
"description": "Rancher UI Extension for Harvester",
|
"description": "Rancher UI Extension for Harvester",
|
||||||
"version": "1.7.0-rc2",
|
"version": "1.7.1-rc4",
|
||||||
"private": false,
|
"private": false,
|
||||||
"rancher": {
|
"rancher": {
|
||||||
"annotations": {
|
"annotations": {
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ export default {
|
|||||||
skipSingleReplicaDetachedVolFeatureEnabled() {
|
skipSingleReplicaDetachedVolFeatureEnabled() {
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
|
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
|
||||||
},
|
},
|
||||||
|
|
||||||
allOSImages() {
|
allOSImages() {
|
||||||
return this.$store.getters['harvester/all'](HCI.IMAGE).filter((I) => I.isOSImage) || [];
|
return this.$store.getters['harvester/all'](HCI.IMAGE).filter((I) => I.isOSImage) || [];
|
||||||
},
|
},
|
||||||
@@ -116,7 +115,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
fileName() {
|
fileName() {
|
||||||
return this.file?.name || '';
|
return this.preprocessImageName(this.file?.name || '');
|
||||||
},
|
},
|
||||||
|
|
||||||
canEnableLogging() {
|
canEnableLogging() {
|
||||||
@@ -191,6 +190,7 @@ export default {
|
|||||||
annotations: {}
|
annotations: {}
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
|
backend: 'cdi',
|
||||||
sourceType: UPLOAD,
|
sourceType: UPLOAD,
|
||||||
displayName: '',
|
displayName: '',
|
||||||
checksum: this.imageValue?.spec?.checksum || '',
|
checksum: this.imageValue?.spec?.checksum || '',
|
||||||
@@ -203,8 +203,9 @@ export default {
|
|||||||
|
|
||||||
this.file = {};
|
this.file = {};
|
||||||
this.errors = [];
|
this.errors = [];
|
||||||
|
const imageDisplayName = this.imageValue?.spec?.displayName || '';
|
||||||
|
|
||||||
if (!this.imageValue.spec.displayName && this.createNewImage) {
|
if (!imageDisplayName && this.createNewImage) {
|
||||||
this.errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('generic.name') }));
|
this.errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('generic.name') }));
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
|
|
||||||
@@ -212,24 +213,29 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Save the image first if creating a new one
|
||||||
if (this.imageSource === IMAGE_METHOD.NEW) {
|
if (this.imageSource === IMAGE_METHOD.NEW) {
|
||||||
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
|
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
|
||||||
|
|
||||||
if (this.sourceType === UPLOAD && this.uploadImageId !== '') { // upload new image
|
if (this.sourceType === UPLOAD && this.uploadImageId !== '') { // upload new image
|
||||||
this.value.spec.image = this.uploadImageId;
|
this.value.spec.image = this.uploadImageId;
|
||||||
} else if (this.sourceType === DOWNLOAD) { // give URL to download new image
|
} else if (this.sourceType === DOWNLOAD) { // give URL to download new image
|
||||||
this.imageValue.spec.sourceType = DOWNLOAD;
|
// check if URL is provided
|
||||||
if (!this.imageValue.spec.url) {
|
if (!this.imageValue.spec.url) {
|
||||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.imageUrl'));
|
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.imageUrl'));
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.imageValue.spec.sourceType = DOWNLOAD;
|
||||||
|
this.imageValue.spec.targetStorageClassName = 'longhorn-static';
|
||||||
|
|
||||||
res = await this.imageValue.save();
|
res = await this.imageValue.save();
|
||||||
|
|
||||||
this.value.spec.image = res.id;
|
this.value.spec.image = res.id;
|
||||||
}
|
}
|
||||||
} else if (this.imageSource === IMAGE_METHOD.EXIST) {
|
} else if (this.imageSource === IMAGE_METHOD.EXIST) { // select existing image
|
||||||
if (!this.imageId) {
|
if (!this.imageId) {
|
||||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.chooseFile'));
|
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.chooseFile'));
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
@@ -239,7 +245,7 @@ export default {
|
|||||||
|
|
||||||
this.value.spec.image = this.imageId;
|
this.value.spec.image = this.imageId;
|
||||||
}
|
}
|
||||||
|
// enable logging or skip single replica detection if checked
|
||||||
if (this.canEnableLogging) {
|
if (this.canEnableLogging) {
|
||||||
this.value.spec.logEnabled = this.enableLogging;
|
this.value.spec.logEnabled = this.enableLogging;
|
||||||
}
|
}
|
||||||
@@ -256,7 +262,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async uploadFile(file) {
|
async uploadFile(file) {
|
||||||
const fileName = file.name;
|
const fileName = this.preprocessImageName(file.name);
|
||||||
|
|
||||||
if (!fileName) {
|
if (!fileName) {
|
||||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.unknownImageName'));
|
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.unknownImageName'));
|
||||||
@@ -280,6 +286,8 @@ export default {
|
|||||||
this.imageValue.spec.url = '';
|
this.imageValue.spec.url = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.imageValue.spec.targetStorageClassName = 'longhorn-static';
|
||||||
|
|
||||||
const res = await this.imageValue.save();
|
const res = await this.imageValue.save();
|
||||||
|
|
||||||
this.uploadImageId = res.id;
|
this.uploadImageId = res.id;
|
||||||
@@ -301,15 +309,25 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// replace _ to - to meet storage class name requirement
|
||||||
|
preprocessImageName(name) {
|
||||||
|
if (!name) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return name.toLowerCase().replace(/[_]/g, '-');
|
||||||
|
},
|
||||||
|
|
||||||
handleImageDelete(imageId) {
|
handleImageDelete(imageId) {
|
||||||
const image = this.allOSImages.find((I) => I.id === imageId);
|
const image = this.allOSImages.find((I) => I.id === imageId);
|
||||||
|
const imageDisplayName = image?.spec?.displayName || '';
|
||||||
|
|
||||||
if (image) {
|
if (image && imageDisplayName) {
|
||||||
this.$store.dispatch('harvester/promptModal', {
|
this.$store.dispatch('harvester/promptModal', {
|
||||||
resources: [image],
|
resources: [image],
|
||||||
component: 'ConfirmRelatedToRemoveDialog',
|
component: 'ConfirmRelatedToRemoveDialog',
|
||||||
needConfirmation: false,
|
needConfirmation: false,
|
||||||
warningMessage: this.$store.getters['i18n/t']('harvester.modal.osImage.message', { name: image.displayName })
|
warningMessage: this.$store.getters['i18n/t']('harvester.modal.osImage.message', { name: imageDisplayName }),
|
||||||
});
|
});
|
||||||
this.deleteImageId = '';
|
this.deleteImageId = '';
|
||||||
}
|
}
|
||||||
@@ -419,13 +437,13 @@ export default {
|
|||||||
v-if="showUploadSuccessBanner"
|
v-if="showUploadSuccessBanner"
|
||||||
color="success"
|
color="success"
|
||||||
class="mt-0 mb-30"
|
class="mt-0 mb-30"
|
||||||
:label="t('harvester.setting.upgrade.uploadSuccess', { name: file.name })"
|
:label="t('harvester.setting.upgrade.uploadSuccess', { name: fileName })"
|
||||||
/>
|
/>
|
||||||
<Banner
|
<Banner
|
||||||
v-if="showUploadingWarningBanner"
|
v-if="showUploadingWarningBanner"
|
||||||
color="warning"
|
color="warning"
|
||||||
class="mt-0 mb-30"
|
class="mt-0 mb-30"
|
||||||
:label="t('harvester.image.warning.osUpgrade.uploading', { name: file.name })"
|
:label="t('harvester.image.warning.osUpgrade.uploading', { name: fileName })"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user