Compare commits

...

46 Commits
main ... v1.7.1

Author SHA1 Message Date
renovate[bot]
bd647738ae
deps: update patch digest dependencies (#706)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-08 09:11:33 +00:00
Andy Lee
c6fb969d7e
chore: update version v1.7.1 (#704)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-02-06 14:48:07 +08:00
Andy Lee
2afb04947d
chore: update v1.7.1-rc4 version
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-02-03 16:42:06 +08:00
Andy Lee
730c68bf14
chore: add v1.7.1 feature flag (#701)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-30 15:11:08 +08:00
freeze
20bee39a6c
chore: bump to v1.7.1-rc3 (#699)
Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
2026-01-29 01:10:39 +08:00
Andy Lee
dbb199d7bb
chore: bump to v1.7.1-rc2
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-27 22:37:48 +08:00
mergify[bot]
5488979448
fix: use longhorn-static for upgrade vmimage (#690) (#692)
(cherry picked from commit 0647600e88b59ccdf6a8d8f78d88b972cd604185)

Signed-off-by: Cooper Tseng <cooper.tseng@suse.com>
2026-01-22 09:51:01 +08:00
mergify[bot]
9e588e90c2
fix: remove isCordoned condition (#689) (#691)
(cherry picked from commit 99dbba7958c5bffb38aa68a3c6f6f0f44706ebae)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-01-21 17:43:08 +08:00
Andy Lee
9378277102
chore: change to v1.7.1-rc1 version
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-01-20 17:34:15 +08:00
mergify[bot]
b5e78018a5
fix: use file as field name instead of chunk in cdi vmimage upload (#684) (#688)
(cherry picked from commit 915559962a91802789750cec7549b61baea096e7)

Signed-off-by: Cooper Tseng <cooper.tseng@suse.com>
Co-authored-by: Kuan-Po Tseng <brandboat@gmail.com>
2026-01-20 11:00:50 +08:00
renovate[bot]
f411a0c0af
deps: update patch digest dependencies (#687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-18 08:48:10 +00:00
mergify[bot]
cfa58985cf
fix: do not set cpu.maxSockets on UI (#674) (#685)
* fix: do not set cpu.maxSockets for ARM clusters



* fix: remove maxSocket to fix bug on ARM cluster



---------


(cherry picked from commit b1b1a31c04a2f0b20fdfee42f987c694614617bf)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-01-16 17:15:34 +08:00
renovate[bot]
66a8f9d0e7
deps: update patch digest dependencies (#680)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-11 08:53:44 +00:00
freeze
bf61c7dd7d
chore: bump version to v1.7.1-dev (#673)
Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
2026-01-04 23:21:46 +08:00
mergify[bot]
56d97260c4
fix: drop mac-address annotation from vm template to prevent MAC address reusing (#663) (#669)
related-to: harvester/harvester#9789

(cherry picked from commit 1352246e1efdab691b7ccba9e1d02da01b6844a1)

Signed-off-by: Tim Liou <tim.liou@suse.com>
Co-authored-by: Tim Liou <tim.liou@suse.com>
2026-01-02 18:13:35 +08:00
mergify[bot]
beabb34920
docs: add README.md in pkg/harvester (#661) (#662)
(cherry picked from commit fe3a12e28ca6b28193b18e665af151078ce46499)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2026-01-02 16:18:06 +08:00
renovate[bot]
d2609157bd
deps: update dependency qs to v6.14.1 [security] (#668)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-02 00:42:31 +00:00
mergify[bot]
8fbe1943d8
chore: bump version to v1.7.0 (#658) (#659)
(cherry picked from commit a86302c9d5f2c1e065fb2cfe9cfd8d927bdc239b)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-12-22 13:41:18 +08:00
mergify[bot]
ec6bc4d639
chore: bump version to v1.7.0-rc7 (#656) (#657)
(cherry picked from commit 5fe7e13fcd6e46f32573e97a8d7bd8710078488e)

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
Co-authored-by: freeze <1615081+Vicente-Cheng@users.noreply.github.com>
2025-12-16 20:27:51 +08:00
renovate[bot]
3824a14730
deps: update dependency @types/node to v20.19.27 (#655)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-14 08:59:31 +00:00
mergify[bot]
0fc8bece02
chore: bump version to v1.7.0-rc6 (#649) (#650)
(cherry picked from commit 57695886336a7553b5ffdf6bb65093fa2037b3e6)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-12-11 16:50:51 +08:00
mergify[bot]
39764af627
fix: failed to create multiple VMs (#647) (#648)
(cherry picked from commit b29950f99cbcaf40919654fee8f6a58201a33574)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-12-11 16:17:02 +08:00
mergify[bot]
bdc87bda0e
fix: do not inherit template secret when creating new VM (#643) (#646)
(cherry picked from commit 6c27a462748575da1fd6e0f04baf116063f7498f)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-12-10 17:18:33 +08:00
mergify[bot]
e0dc77624b
feat: read addon displayname from label and add descheduler description (#644) (#645)
* refactor: display addon name from addon.harvesterhci.io/displayName label



* refactor: add descheduler description



---------


(cherry picked from commit b03fffbc3014dc7214177cac69bfcecdf7cb30c3)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-12-10 15:16:08 +08:00
renovate[bot]
c3ba10bd22
deps: update dependency node-forge to v1.3.3 (#641)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-07 09:15:44 +00:00
mergify[bot]
5b7d54d0a3
chore: bump to v1.7.0-rc5 (#636) (#637)
(cherry picked from commit 416098ffd822ff531d8cfa42fdbfed1c8d53ff53)

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
Co-authored-by: freeze <1615081+Vicente-Cheng@users.noreply.github.com>
2025-12-04 22:45:15 +08:00
renovate[bot]
99a216dfa0
deps: update dependency yaml to v2.8.2 (#634)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-30 16:55:46 +00:00
mergify[bot]
e4c85f510e
chore: bump to v1.7.0-rc4 (#621) (#629)
(cherry picked from commit 3d7b96d86d3fce8f7746f1eca1a90ea9b31bb67d)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-28 13:23:55 +08:00
mergify[bot]
f391f018de
fix: create new secret on vm creation (#614) (#628)
* fix: create new secret on vm creation



* fix: ensure parseVM result is immutable



---------


(cherry picked from commit 0b37467f7637639209c27570bfe5633a41b96ac0)

Signed-off-by: Caio Torres <caio.torres@suse.com>
Co-authored-by: Caio Torres <caio.torres@suse.com>
2025-11-27 17:42:46 +08:00
renovate[bot]
4b2e92ea15
deps: update dependency node-forge to v1.3.2 [security] (#627)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-27 14:31:50 +08:00
mergify[bot]
2f956d5946
feat: allow user to attach volume to muliple VMs (#620) (#622)
(cherry picked from commit d94003f8c28876b4e5803bb04a6049ab63f812e6)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-27 14:30:44 +08:00
renovate[bot]
5f8d556ea2
deps: update patch digest dependencies (#619)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-23 09:32:21 +00:00
mergify[bot]
396ab48f1c
chore: bump version to v1.7.0-rc3 (#612) (#613) 2025-11-20 10:25:03 +08:00
mergify[bot]
6b8c079018
fix: condition render namespaceOptions (#607) (#611)
(cherry picked from commit 1b183febdc5e13d29d48b655198114f7d38af526)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-11-19 17:40:03 +08:00
mergify[bot]
7f638e86c8
fix: change migConfiguration model to inherit from harvester resource (#608) (#609)
(cherry picked from commit 70d3b656f78439ba56816d75b526bc5637a67e1f)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-19 17:35:52 +08:00
mergify[bot]
0c4955a766
feat: create related image storageclass before OS upgrade (#595) (#606)
* feat: create related image SC before upgrade



* refactor: update spec.targetStorageClassName



* refactor: based on comment



---------


(cherry picked from commit 10d19cd329cce7e376ce2712a8843742d8968b65)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-17 17:34:51 +08:00
mergify[bot]
3f4ff30275
feat: modified placeholder (#599) (#605)
(cherry picked from commit 1715ae754caf4cb6e0688d8eb4326f9b0a90f960)

Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-11-17 17:08:20 +08:00
renovate[bot]
8e0332a364
deps: update dependency @types/node to v20.19.25 (#604)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-16 13:03:17 +00:00
mergify[bot]
6700b2055e
feat: add support for configuring transparent hugepages (#414) (#598)
* feat: add support for configuring transparent hugepages

Related-to: https://github.com/harvester/harvester/issues/5006



* fix: return empty object if hugepages can't be found for node

Related-to: https://github.com/harvester/harvester/issues/5006




---------




(cherry picked from commit 30de2b1a185ccc2a3ec159e220de742dd2156229)

Signed-off-by: Tim Serong <tserong@suse.com>
Co-authored-by: Tim Serong <tserong@suse.com>
Co-authored-by: Moritz Röhrich <moritz.rohrich@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-12 14:15:27 +08:00
mergify[bot]
e486852f7a
chore: bump version to v1.7.0-rc2 (#596) (#597)
(cherry picked from commit 6fedcc353c59df9e36693822f56c0be78029a46a)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-12 09:21:01 +08:00
mergify[bot]
c19341bec9
feat: support for HotPlugNICs from Kubevirt (#582) (#594)
* refactor: rename hotplug volume
* feat: add hotplug NIC
* feat: add hot unplug
* refactor: rename NIC
* feat: get latest status
* feat: disable not ready options
* feat: filter out system networks
* refactor: update wordings

---------


(cherry picked from commit f9bff21e840885a120679864a0ef312163bd48a7)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-11-11 11:46:43 +08:00
mergify[bot]
8cbb9d6b18
chore: update yarn.lock for @rancher/shell v3.0.8-rc.8 (#591) (#592)
(cherry picked from commit 6735826e15d6515f87fa752418141ebb79cc5c1f)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-10 16:19:48 +08:00
mergify[bot]
00f0953592
chore: bump shell version to v3.0.8-rc.8 (#588) (#590)
(cherry picked from commit 9e17e239cf9d6e65f5d2f42636bac19cef70bb16)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-10 15:05:04 +08:00
mergify[bot]
58507f0b2e
ci: lint last commit if is empty string or all zero (#584) (#586)
(cherry picked from commit db58024351e06c6ddc90d6143cae9133ddbbfedc)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-06 15:53:50 +08:00
mergify[bot]
7785d7f469
feat: enable snapshot and clone for LHv2 (#379) (#587)
Now that Longhorn supports volume clone with the V2 data engine, we
can enable volume snapshot and clone.

Related issue: https://github.com/harvester/harvester/issues/6710


(cherry picked from commit a1cf41bda92ceb399be21904ec267311f9568bdb)

Signed-off-by: Tim Serong <tserong@suse.com>
Co-authored-by: Tim Serong <tserong@suse.com>
2025-11-06 15:53:20 +08:00
mergify[bot]
2c043e0a8e
chore: bump version to v1.7.0-rc1 (#583) (#585)
(cherry picked from commit 81bf19419c56dbd670d4cf8a1b9b658bbae6ea4f)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-04 16:12:14 +08:00
34 changed files with 2104 additions and 814 deletions

View File

@ -41,8 +41,14 @@ jobs:
FROM="$GITHUB_BASE_SHA" FROM="$GITHUB_BASE_SHA"
TO="$GITHUB_HEAD_SHA" TO="$GITHUB_HEAD_SHA"
elif [ -n "$GITHUB_BEFORE" ] && [ -n "$GITHUB_AFTER" ]; then elif [ -n "$GITHUB_BEFORE" ] && [ -n "$GITHUB_AFTER" ]; then
FROM="$GITHUB_BEFORE" if [ "$GITHUB_BEFORE" = "0000000000000000000000000000000000000000" ]; then
TO="$GITHUB_AFTER" # first push to HEAD
FROM=""
TO="$GITHUB_AFTER"
else
FROM="$GITHUB_BEFORE"
TO="$GITHUB_AFTER"
fi
else else
echo "No valid commit range found, skipping commitlint." echo "No valid commit range found, skipping commitlint."
exit 0 exit 0
@ -51,7 +57,14 @@ jobs:
echo "FROM=$FROM" echo "FROM=$FROM"
echo "TO=$TO" echo "TO=$TO"
npx commitlint --from "$FROM" --to "$TO" --verbose if [ -z "$FROM" ]; then
echo "Linting last commit $TO"
npx commitlint --last --verbose
else
echo "Linting commits from $FROM to $TO"
npx commitlint --from "$FROM" --to "$TO" --verbose
fi
env: env:
GITHUB_EVENT_NAME: ${{ github.event_name }} GITHUB_EVENT_NAME: ${{ github.event_name }}
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }} GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}

View File

@ -1,13 +1,13 @@
{ {
"name": "harvester-ui-extension", "name": "harvester-ui-extension",
"version": "1.7.0-dev", "version": "1.7.1",
"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.5-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",
"ip": "2.0.1", "ip": "2.0.1",
@ -24,13 +24,13 @@
"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.4",
"@vue/cli-service/html-webpack-plugin": "^5.0.0" "@vue/cli-service/html-webpack-plugin": "^5.0.0"
}, },
"scripts": { "scripts": {

178
pkg/harvester/README.md Normal file
View File

@ -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.

View File

@ -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];
} }
}, },

View File

@ -9,4 +9,5 @@ export const DOC = {
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`, SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
VPC_CONFIGURATION_EXAMPLES: `/networking/kubeovn-vpc#vpc-peering-configuration-examples`, VPC_CONFIGURATION_EXAMPLES: `/networking/kubeovn-vpc#vpc-peering-configuration-examples`,
NETWORK_POLICY: `/networking/kubeovn-vm-isolation/#network-policies`, NETWORK_POLICY: `/networking/kubeovn-vm-isolation/#network-policies`,
TRANSPARENT_HUGEPAGES: `https://docs.kernel.org/admin-guide/mm/transhuge.html`,
}; };

View File

@ -53,8 +53,10 @@ const FEATURE_FLAGS = {
'vmMachineTypeAuto', 'vmMachineTypeAuto',
'lhV2VolExpansion', 'lhV2VolExpansion',
'l2VlanTrunkMode', 'l2VlanTrunkMode',
'kubevirtMigration' 'kubevirtMigration',
] 'hotplugNic'
],
'v1.7.1': [],
}; };
const generateFeatureFlags = () => { const generateFeatureFlags = () => {

View File

@ -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',
}; };

View File

@ -0,0 +1,119 @@
<script>
import LabelValue from '@shell/components/LabelValue';
import { HCI } from '../../types';
import { DOC } from '../../config/doc-links';
export default {
name: 'HarvesterHugepages',
components: { LabelValue },
props: {
node: {
type: Object,
required: true,
},
},
computed: {
docsTransparentHugepagesLink() {
return DOC.TRANSPARENT_HUGEPAGES;
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
this.hugepages = hash.find((node) => {
return node.id === this.node.id;
}) || {};
},
data() {
return { hugepages: {} };
},
};
</script>
<template>
<div>
<template v-if="hugepages.status">
<h2>{{ t('harvester.host.hugepages.meminfo') }}</h2>
<div class="row mb-20">
<div class="col span-6">
<LabelValue
:name="t('harvester.host.hugepages.status.anon')"
:value="hugepages.status.meminfo.anonHugePages"
/>
</div>
<div class="col span-6">
<LabelValue
:name="t('harvester.host.hugepages.status.size')"
:value="hugepages.status.meminfo.hugepageSize"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-3">
<LabelValue
:name="t('harvester.host.hugepages.status.total')"
:value="hugepages.status.meminfo.hugePagesTotal"
/>
</div>
<div class="col span-3">
<LabelValue
:name="t('harvester.host.hugepages.status.free')"
:value="hugepages.status.meminfo.hugePagesFree"
/>
</div>
<div class="col span-3">
<LabelValue
:name="t('harvester.host.hugepages.status.rsvd')"
:value="hugepages.status.meminfo.hugePagesRsvd"
/>
</div>
<div class="col span-3">
<LabelValue
:name="t('harvester.host.hugepages.status.surp')"
:value="hugepages.status.meminfo.hugePagesSurp"
/>
</div>
</div>
<div>
<hr class="divider" />
<h3>
<t
k="harvester.host.hugepages.transparent.title"
:raw="true"
:url="docsTransparentHugepagesLink"
/>
</h3>
<div class="row mb-20">
<div class="col span-4">
<LabelValue
:name="t('harvester.host.hugepages.transparent.enabled')"
:value="hugepages.spec.transparent.enabled"
/>
</div>
<div class="col span-4">
<LabelValue
:name="t('harvester.host.hugepages.transparent.shmemEnabled')"
:value="hugepages.spec.transparent.shmemEnabled"
/>
</div>
<div class="col span-4">
<LabelValue
:name="t('harvester.host.hugepages.transparent.defrag')"
:value="hugepages.spec.transparent.defrag"
/>
</div>
</div>
</div>
</template>
</div>
</template>

View File

@ -27,6 +27,7 @@ import Instance from './VirtualMachineInstance';
import Disk from './HarvesterHostDisk'; import Disk from './HarvesterHostDisk';
import VlanStatus from './VlanStatus'; import VlanStatus from './VlanStatus';
import HarvesterKsmtuned from './HarvesterKsmtuned.vue'; import HarvesterKsmtuned from './HarvesterKsmtuned.vue';
import HarvesterHugepages from './HarvesterHugepages.vue';
import HarvesterSeeder from './HarvesterSeeder'; import HarvesterSeeder from './HarvesterSeeder';
const LONGHORN_SYSTEM = 'longhorn-system'; const LONGHORN_SYSTEM = 'longhorn-system';
@ -46,6 +47,7 @@ export default {
VlanStatus, VlanStatus,
LabelValue, LabelValue,
HarvesterKsmtuned, HarvesterKsmtuned,
HarvesterHugepages,
Loading, Loading,
SortableTable, SortableTable,
HarvesterSeeder, HarvesterSeeder,
@ -209,6 +211,12 @@ export default {
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED); return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
}, },
hasHugepagesSchema() {
const inStore = this.$store.getters['currentProduct'].inStore;
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.HUGEPAGES);
},
hasBlockDevicesSchema() { hasBlockDevicesSchema() {
return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE); return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE);
}, },
@ -468,6 +476,16 @@ export default {
/> />
</Tab> </Tab>
<Tab
v-if="hasHugepagesSchema"
name="hugepages"
:weight="0"
:show-header="false"
:label="t('harvester.host.tabs.hugepages')"
>
<HarvesterHugepages :node="value" />
</Tab>
<Tab <Tab
v-if="seederEnabled" v-if="seederEnabled"
name="seeder" name="seeder"

View File

@ -213,6 +213,7 @@ export default {
const diskRows = this.getDiskRows(neu); const diskRows = this.getDiskRows(neu);
this['diskRows'] = diskRows; this['diskRows'] = diskRows;
this['networkRows'] = this.getNetworkRows(neu, { fromTemplate: false, init: false });
}, },
deep: true deep: true
} }
@ -265,6 +266,7 @@ export default {
<Network <Network
v-model:value="networkRows" v-model:value="networkRows"
mode="view" mode="view"
:vm="value"
/> />
</Tab> </Tab>

View File

@ -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();

View File

@ -0,0 +1,212 @@
<script>
import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex';
import { NETWORK_ATTACHMENT } from '@shell/config/types';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { NETWORK_TYPE } from '../config/types';
export default {
name: 'AddHotplugNic',
emits: ['close'],
components: {
AsyncButton,
Card,
LabeledInput,
LabeledSelect,
Banner
},
props: {
resources: {
type: Array,
required: true
}
},
async fetch() {
try {
this.allVMNetworks = await this.$store.dispatch('harvester/findAll', { type: NETWORK_ATTACHMENT });
} catch (err) {
this.errors = exceptionToErrorsArray(err);
this.allVMNetworks = [];
}
},
data() {
return {
interfaceName: '',
networkName: '',
macAddress: '',
allVMNetworks: [],
errors: [],
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources?.[0];
},
isFormValid() {
return this.interfaceName !== '' && this.networkName !== '';
},
vmNetworksOption() {
return this.allVMNetworks
.filter((network) => {
const labels = network.metadata?.labels || {};
const type = labels[HCI_ANNOTATIONS.NETWORK_TYPE];
const isValidType = [
NETWORK_TYPE.L2VLAN,
NETWORK_TYPE.UNTAGGED,
NETWORK_TYPE.L2TRUNK_VLAN,
].includes(type);
return isValidType && !network.isSystem;
})
.map((network) => {
const label = network.isNotReady ? `${ network.id } (${ this.t('generic.notReady') })` : network.id;
return ({
label,
value: network.id || '',
disabled: network.isNotReady,
});
});
}
},
methods: {
close() {
this.interfaceName = '';
this.networkName = '';
this.macAddress = '';
this.errors = [];
this.$emit('close');
},
async save(buttonCb) {
if (!this.actionResource) {
buttonCb(false);
return;
}
const payload = {
interfaceName: this.interfaceName,
networkName: this.networkName
};
if (this.macAddress) {
payload.macAddress = this.macAddress;
}
try {
const res = await this.actionResource.doAction('addNic', payload);
if ([200, 204].includes(res?._status)) {
this.$store.dispatch('growl/success', {
title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.modal.hotplugNic.success', {
interfaceName: this.interfaceName,
vm: this.actionResource.nameDisplay
})
}, { root: true });
this.close();
buttonCb(true);
} else {
this.errors = exceptionToErrorsArray(res);
buttonCb(false);
}
} catch (err) {
this.errors = exceptionToErrorsArray(err);
buttonCb(false);
}
}
}
};
</script>
<template>
<Card
ref="modal"
name="modal"
:show-highlight-border="false"
>
<template #title>
<h4
v-clean-html="t('harvester.modal.hotplugNic.title')"
class="text-default-text"
/>
</template>
<template #body>
<LabeledInput
v-model:value="interfaceName"
:label="t('generic.name')"
required
/>
<LabeledSelect
v-model:value="networkName"
class="mt-20"
:label="t('harvester.modal.hotplugNic.vmNetwork')"
:options="vmNetworksOption"
required
/>
<LabeledInput
v-model:value="macAddress"
class="mt-20"
label-key="harvester.modal.hotplugNic.macAddress"
:tooltip="t('harvester.modal.hotplugNic.macAddressTooltip', _, true)"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
:label="err"
color="error"
/>
</template>
<template #actions>
<div class="actions">
<div class="buttons">
<button
type="button"
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="apply"
:disabled="!isFormValid"
@click="save"
/>
</div>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>

View File

@ -11,7 +11,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
export default { export default {
name: 'HotplugModal', name: 'HotplugVolumeModal',
emits: ['close'], emits: ['close'],
@ -62,7 +62,7 @@ export default {
return false; return false;
} }
return !pvc.attachVM; return true;
}) })
.map((pvc) => { .map((pvc) => {
return { return {
@ -90,7 +90,7 @@ export default {
if (res._status === 200 || res._status === 204) { if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', { this.$store.dispatch('growl/success', {
title: this.t('generic.notification.title.succeed'), title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.modal.hotplug.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay }) message: this.t('harvester.modal.hotplugVolume.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
}, { root: true }); }, { root: true });
this.close(); this.close();
@ -122,7 +122,7 @@ export default {
> >
<template #title> <template #title>
<h4 <h4
v-clean-html="t('harvester.modal.hotplug.title')" v-clean-html="t('harvester.modal.hotplugVolume.title')"
class="text-default-text" class="text-default-text"
/> />
</template> </template>

View File

@ -1,13 +1,12 @@
<script> <script>
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import { exceptionToErrorsArray } from '@shell/utils/error'; import { exceptionToErrorsArray } from '@shell/utils/error';
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';
export default { export default {
name: 'HarvesterHotUnplugModal', name: 'HarvesterHotUnplug',
emits: ['close'], emits: ['close'],
@ -35,8 +34,25 @@ export default {
actionResource() { actionResource() {
return this.resources[0]; return this.resources[0];
}, },
diskName() {
return this.modalData.diskName; name() {
return this.modalData.name;
},
isVolume() {
return this.modalData.type === 'volume';
},
titleKey() {
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.title' : 'harvester.virtualMachine.hotUnplug.detachNIC.title';
},
actionLabelKey() {
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabel' : 'harvester.virtualMachine.hotUnplug.detachNIC.actionLabel';
},
successMessageKey() {
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.success' : 'harvester.virtualMachine.hotUnplug.detachNIC.success';
} }
}, },
@ -47,14 +63,20 @@ export default {
async save(buttonCb) { async save(buttonCb) {
try { try {
const res = await this.actionResource.doAction('removeVolume', { diskName: this.diskName }); let res;
if (this.isVolume) {
res = await this.actionResource.doAction('removeVolume', { diskName: this.name });
} else {
res = await this.actionResource.doAction('removeNic', { interfaceName: this.name });
}
if (res._status === 200 || res._status === 204) { if (res._status === 200 || res._status === 204) {
this.$store.dispatch( this.$store.dispatch(
'growl/success', 'growl/success',
{ {
title: this.t('generic.notification.title.succeed'), title: this.t('generic.notification.title.succeed'),
message: this.t('harvester.modal.hotunplug.success', { name: this.diskName }) message: this.t(this.successMessageKey, { name: this.name })
}, },
{ root: true } { root: true }
); );
@ -64,14 +86,14 @@ export default {
} else { } else {
const error = [res?.data] || exceptionToErrorsArray(res); const error = [res?.data] || exceptionToErrorsArray(res);
this['errors'] = error; this.errors = error;
buttonCb(false); buttonCb(false);
} }
} catch (err) { } catch (err) {
const error = err?.data || err; const error = err?.data || err;
const message = exceptionToErrorsArray(error); const message = exceptionToErrorsArray(error);
this['errors'] = message; this.errors = message;
buttonCb(false); buttonCb(false);
} }
} }
@ -87,7 +109,7 @@ export default {
> >
<template #title> <template #title>
<h4 <h4
v-clean-html="t('harvester.virtualMachine.unplug.title', { name: diskName })" v-clean-html="t(titleKey, { name })"
class="text-default-text" class="text-default-text"
/> />
<Banner <Banner
@ -111,9 +133,9 @@ export default {
<AsyncButton <AsyncButton
mode="apply" mode="apply"
:action-label="t('harvester.virtualMachine.unplug.actionLabel')" :action-label="t(actionLabelKey)"
:waiting-label="t('harvester.virtualMachine.unplug.actionLabel')" :waiting-label="t(actionLabelKey)"
:success-label="t('harvester.virtualMachine.unplug.actionLabel')" :success-label="t(actionLabelKey)"
@click="save" @click="save"
/> />
</div> </div>
@ -132,4 +154,8 @@ export default {
justify-content: flex-end; justify-content: flex-end;
width: 100%; width: 100%;
} }
::v-deep(.card-title) {
display: block;
}
</style> </style>

View File

@ -0,0 +1,157 @@
<script>
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
import { HCI } from '../../types';
import { DOC } from '../../config/doc-links';
export const hugepagesTHPEnabledMode = [{
label: 'Always',
value: 'always',
}, {
label: 'Madvise',
value: 'madvise',
}, {
label: 'Never',
value: 'never',
}];
export const hugepagesTHPShmemEnabledMode = [{
label: 'Always',
value: 'always',
}, {
label: 'Within Size',
value: 'within_size',
}, {
label: 'Advise',
value: 'advise',
}, {
label: 'Never',
value: 'never',
}, {
label: 'Deny',
value: 'deny',
}, {
label: 'Force',
value: 'force',
}];
export const hugepagesTHPDefragMode = [{
label: 'Always',
value: 'always',
}, {
label: 'Defer',
value: 'defer',
}, {
label: 'Defer+Madvise',
value: 'defer+madvise',
}, {
label: 'Madvise',
value: 'madvise',
}, {
label: 'Never',
value: 'never'
}];
export default {
name: 'HarvesterHugepages',
components: { LabeledSelect },
props: {
node: {
type: Object,
required: true,
},
registerBeforeHook: {
type: Function,
required: true,
},
},
computed: {
docsTransparentHugepagesLink() {
return DOC.TRANSPARENT_HUGEPAGES;
},
},
methods: {
async saveHugepages() {
this.hugepages['spec'] = this.spec;
await this.hugepages.save().catch((reason) => {
if (reason?.type === 'error') {
this.$store.dispatch('growl/error', {
title: this.t('harvester.notification.title.error'),
message: reason?.message,
}, { root: true });
return Promise.reject(new Error('saveHugepages error'));
}
});
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
this.hugepages = hash.find((node) => {
return node.id === this.node.id;
});
this.spec = this.hugepages.spec;
},
data() {
return {
hugepages: {},
spec: { transparent: {} },
hugepagesTHPEnabledMode,
hugepagesTHPShmemEnabledMode,
hugepagesTHPDefragMode,
};
},
created() {
this.registerBeforeHook(this.saveHugepages, 'saveHugepages');
},
};
</script>
<template>
<div>
<div>
<hr class="divider" />
<h3>
<t
k="harvester.host.hugepages.transparent.title"
:raw="true"
:url="docsTransparentHugepagesLink"
/>
</h3>
<div class="row mb-20">
<div class="col span-4">
<LabeledSelect
v-model:value="spec.transparent.enabled"
:label="t('harvester.host.hugepages.transparent.enabled')"
:options="hugepagesTHPEnabledMode"
/>
</div>
<div class="col span-4">
<LabeledSelect
v-model:value="spec.transparent.shmemEnabled"
:label="t('harvester.host.hugepages.transparent.shmemEnabled')"
:options="hugepagesTHPShmemEnabledMode"
/>
</div>
<div class="col span-4">
<LabeledSelect
v-model:value="spec.transparent.defrag"
:label="t('harvester.host.hugepages.transparent.defrag')"
:options="hugepagesTHPDefragMode"
/>
</div>
</div>
</div>
</div>
</template>

View File

@ -28,6 +28,7 @@ import { HCI } from '../../types';
import HarvesterDisk from './HarvesterDisk'; import HarvesterDisk from './HarvesterDisk';
import HarvesterSeeder from './HarvesterSeeder'; import HarvesterSeeder from './HarvesterSeeder';
import HarvesterKsmtuned from './HarvesterKsmtuned'; import HarvesterKsmtuned from './HarvesterKsmtuned';
import HarvesterHugepages from './HarvesterHugepages';
import Tags from '../../components/DiskTags'; import Tags from '../../components/DiskTags';
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass'; import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
@ -50,6 +51,7 @@ export default {
ArrayListGrouped, ArrayListGrouped,
HarvesterDisk, HarvesterDisk,
HarvesterKsmtuned, HarvesterKsmtuned,
HarvesterHugepages,
ButtonDropdown, ButtonDropdown,
KeyValue, KeyValue,
Banner, Banner,
@ -225,6 +227,12 @@ export default {
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED); return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
}, },
hasHugepagesSchema() {
const inStore = this.$store.getters['currentProduct'].inStore;
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.HUGEPAGES);
},
hasBlockDevicesSchema() { hasBlockDevicesSchema() {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
@ -647,6 +655,17 @@ export default {
</template> </template>
</ArrayListGrouped> </ArrayListGrouped>
</Tab> </Tab>
<Tab
v-if="hasHugepagesSchema"
name="Hugepages"
:weight="70"
:label="t('harvester.host.tabs.hugepages')"
>
<HarvesterHugepages
:node="value"
:register-before-hook="registerBeforeHook"
/>
</Tab>
<Tab <Tab
v-if="hasKsmtunedSchema" v-if="hasKsmtunedSchema"
name="Ksmtuned" name="Ksmtuned"

View File

@ -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>

View File

@ -1,6 +1,5 @@
<script> <script>
import InfoBox from '@shell/components/InfoBox'; import InfoBox from '@shell/components/InfoBox';
import { NETWORK_ATTACHMENT } from '@shell/config/types'; import { NETWORK_ATTACHMENT } from '@shell/config/types';
import { sortBy } from '@shell/utils/sort'; import { sortBy } from '@shell/utils/sort';
import { clone } from '@shell/utils/object'; import { clone } from '@shell/utils/object';
@ -14,6 +13,13 @@ export default {
components: { InfoBox, Base }, components: { InfoBox, Base },
props: { props: {
vm: {
type: Object,
default: () => {
return {};
}
},
mode: { mode: {
type: String, type: String,
default: 'create' default: 'create'
@ -32,10 +38,15 @@ export default {
} }
}, },
async fetch() {
await this.fetchHotunplugData();
},
data() { data() {
return { return {
rows: this.addKeyId(clone(this.value)), nameIdx: 1,
nameIdx: 1 rows: this.addKeyId(clone(this.value)),
hotunpluggableNics: new Set(),
}; };
}, },
@ -64,15 +75,57 @@ export default {
return out; return out;
}, },
canCheckHotunplug() {
return !!this.vm?.actions?.findHotunpluggableNics;
},
vmState() {
return this.vm?.stateDisplay;
}
}, },
watch: { watch: {
value(neu) { value(neu) {
this.rows = neu; this.rows = this.mergeHotplugData(clone(neu));
}, },
vmState(newState, oldState) {
if (newState !== oldState) {
this.fetchHotunplugData();
}
}
}, },
methods: { methods: {
async fetchHotunplugData() {
if (!this.canCheckHotunplug) {
this.rows = this.mergeHotplugData(clone(this.value));
return;
}
try {
const resp = await this.vm.doAction('findHotunpluggableNics');
this.hotunpluggableNics = new Set(resp?.interfaces || []);
} catch (e) {
// eslint-disable-next-line no-console
console.error('Failed to fetch hot-unpluggable NICs:', e);
this.hotunpluggableNics = new Set();
}
this.rows = this.mergeHotplugData(clone(this.value));
},
mergeHotplugData(networks) {
return (networks || []).map((network) => ({
...network,
isHotunpluggable: this.hotunpluggableNics.has(network.name),
rowKeyId: network.rowKeyId || randomStr(10)
}));
},
add(type) { add(type) {
const name = this.generateName(); const name = this.generateName();
@ -118,7 +171,11 @@ export default {
update() { update() {
this.$emit('update:value', this.rows); this.$emit('update:value', this.rows);
} },
unplugNIC(network) {
this.vm.unplugNIC(network.name);
},
} }
}; };
</script> </script>
@ -129,17 +186,28 @@ export default {
v-for="(row, i) in rows" v-for="(row, i) in rows"
:key="i" :key="i"
> >
<button <div class="box-title mb-10">
v-if="!isView" <h3>
type="button" {{ t('harvester.virtualMachine.network.title') }}
class="role-link remove-vol" </h3>
@click="remove(row)" <button
> v-if="!isView"
<i class="icon icon-x" /> type="button"
</button> class="role-link btn btn-sm remove"
@click="remove(row)"
<h3> {{ t('harvester.virtualMachine.network.title') }} </h3> >
<i class="icon icon-x" />
</button>
<button
v-if="vm.hotplugNicFeatureEnabled && row.isHotunpluggable && isView"
type="button"
class="role-link btn btn-sm remove"
:disabled="!canCheckHotunplug"
@click="unplugNIC(row)"
>
{{ t('harvester.virtualMachine.hotUnplug.detachNIC.actionLabel') }}
</button>
</div>
<Base <Base
v-model:value="rows[i]" v-model:value="rows[i]"
:rows="rows" :rows="rows"
@ -162,16 +230,13 @@ export default {
</template> </template>
<style lang='scss' scoped> <style lang='scss' scoped>
.infoBox{ .box-title{
position: relative; display: flex;
} justify-content: space-between;
align-items: center;
.remove-vol { h3 {
position: absolute; margin-bottom: 0;
top: 10px; }
right: 16px;
padding:0px;
max-height: 28px;
min-height: 28px;
} }
</style> </style>

View File

@ -303,55 +303,57 @@ export default {
v-for="(volume, i) in rows" v-for="(volume, i) in rows"
:key="volume.id" :key="volume.id"
> >
<InfoBox class="box"> <InfoBox>
<button <div class="box-title mb-10">
v-if="!isView" <h3>
type="button" <span
class="role-link btn btn-sm remove" v-if="volume.to && isVirtualType"
@click="removeVolume(volume)" class="title"
>
<i class="icon icon-x" />
</button>
<button
v-if="volume.hotpluggable && isView"
type="button"
class="role-link btn remove"
@click="unplugVolume(volume)"
>
{{ t('harvester.virtualMachine.unplug.detachVolume') }}
</button>
<h3>
<span
v-if="volume.to && isVirtualType"
class="title"
>
<router-link :to="volume.to">
{{ t('harvester.virtualMachine.volume.edit') }} {{ headerFor(volume.source) }}
</router-link>
<BadgeStateFormatter
v-if="volume.pvc"
class="ml-10 state"
:arbitrary="true"
:row="volume.pvc"
:value="volume.pvc.state"
/>
<a
v-if="dev && !!volume.pvc && !!volume.pvc.resourceExternalLink"
v-clean-tooltip="t(volume.pvc.resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
class="ml-5 resource-external"
rel="nofollow noopener noreferrer"
target="_blank"
:href="volume.pvc.resourceExternalLink.url"
> >
<i class="icon icon-external-link" /> <router-link :to="volume.to">
</a> {{ t('harvester.virtualMachine.volume.edit') }} {{ headerFor(volume.source) }}
</span> </router-link>
<span v-else> <BadgeStateFormatter
{{ headerFor(volume.source, !!volume?.volumeBackups) }} v-if="volume.pvc"
</span> class="ml-10 state"
</h3> :arbitrary="true"
:row="volume.pvc"
:value="volume.pvc.state"
/>
<a
v-if="dev && !!volume.pvc && !!volume.pvc.resourceExternalLink"
v-clean-tooltip="t(volume.pvc.resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
class="ml-5 resource-external"
rel="nofollow noopener noreferrer"
target="_blank"
:href="volume.pvc.resourceExternalLink.url"
>
<i class="icon icon-external-link" />
</a>
</span>
<span v-else>
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
</span>
</h3>
<button
v-if="!isView"
type="button"
class="role-link btn btn-sm remove"
@click="removeVolume(volume)"
>
<i class="icon icon-x" />
</button>
<button
v-if="volume.hotpluggable && isView"
type="button"
class="role-link btn btn-sm remove"
@click="unplugVolume(volume)"
>
{{ t('harvester.virtualMachine.hotUnplug.detachVolume.actionLabel') }}
</button>
</div>
<div> <div>
<component <component
:is="componentFor(volume.source)" :is="componentFor(volume.source)"
@ -495,25 +497,24 @@ export default {
</template> </template>
<style lang='scss' scoped> <style lang='scss' scoped>
.box { .box-title {
position: relative; display: flex;
justify-content: space-between;
align-items: center;
h3 {
margin-bottom: 0;
}
} }
.title { .title {
display: flex; display: flex;
align-items: center;
.state { .state {
font-size: 16px; font-size: 16px;
} }
} }
.remove {
position: absolute;
top: 10px;
right: 10px;
padding: 0px;
}
.bootOrder { .bootOrder {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -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 {

View File

@ -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;
} }
} }

View File

@ -154,15 +154,19 @@ harvester:
nodeTimeout: nodeTimeout:
label: Node Collection Timeout label: Node Collection Timeout
tooltip: Minutes allowed for collecting logs/configurations on nodes.<br/>See docs support-bundle-node-collection-timeout for detail. tooltip: Minutes allowed for collecting logs/configurations on nodes.<br/>See docs support-bundle-node-collection-timeout for detail.
hotplug: hotplugVolume:
success: 'Volume { diskName } is mounted to the virtual machine { vm }.' success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
title: Add Volume title: Add Volume
hotplugNic:
success: 'The settings have been saved, but the network interface {interfaceName} will be attached only after the virtual machine is migrated.'
title: Add Network Interface
vmNetwork: Virtual Machine Network
macAddress: MAC Address
macAddressTooltip: If left blank, the MAC address will be automatically generated.
cpuMemoryHotplug: cpuMemoryHotplug:
success: 'CPU and Memory are updated to the virtual machine { vm }.' success: 'CPU and Memory are updated to the virtual machine { vm }.'
title: Edit CPU and Memory title: Edit CPU and Memory
maxResourcesMessage: 'You can increase the CPU to maximum { maxCpu }C and memory to maximum { maxMemory }.' maxResourcesMessage: 'You can increase the CPU to maximum { maxCpu }C and memory to maximum { maxMemory }.'
hotunplug:
success: 'Volume { name } is detached successfully.'
snapshot: snapshot:
title: Take Snapshot title: Take Snapshot
name: Name name: Name
@ -226,7 +230,8 @@ harvester:
disableCPUManager: Disable CPU Manager disableCPUManager: Disable CPU Manager
cordon: Cordon cordon: Cordon
uncordon: Uncordon uncordon: Uncordon
addHotplug: Add Volume addHotplugVolume: Add Volume
addHotplugNic: Hotplug Network Interface
exportImage: Export Image exportImage: Export Image
viewlogs: View Logs viewlogs: View Logs
cancelExpand: Cancel Expand cancelExpand: Cancel Expand
@ -442,6 +447,7 @@ harvester:
storage: Storage storage: Storage
labels: Labels labels: Labels
ksmtuned: Ksmtuned ksmtuned: Ksmtuned
hugepages: Hugepages
seeder: Out-of-band Access seeder: Out-of-band Access
detail: detail:
kvm: kvm:
@ -514,6 +520,20 @@ harvester:
fullScans: Full Scans fullScans: Full Scans
stableNodeChains: Stable Node Chains stableNodeChains: Stable Node Chains
stableNodeDups: Stable Node Dups stableNodeDups: Stable Node Dups
hugepages:
meminfo: Meminfo
transparent:
title: Transparent Hugepages <a href="{url}" target="_blank"><i class="icon icon-info" /></a>
enabled: Enabled
shmemEnabled: Shared Memory Enabled
defrag: Defragmentation
status:
anon: Anonymous Hugepages (bytes)
size: Default Hugepage Size (bytes)
total: Total Hugepages
free: Free Hugepages
rsvd: Reserved Hugepages
surp: Surplus Hugepages
disk: disk:
add: Add Disk add: Add Disk
path: path:
@ -582,6 +602,16 @@ harvester:
title: Enable CPU and memory hotplug title: Enable CPU and memory hotplug
tooltip: The default maximum CPU and maximum memory are {hotPlugTimes} times based on CPU and memory. tooltip: The default maximum CPU and maximum memory are {hotPlugTimes} times based on CPU and memory.
restartVMMessage: Restart action is required for the virtual machine configuration change to take effect restartVMMessage: Restart action is required for the virtual machine configuration change to take effect
hotUnplug:
actionLabel: Detach
detachVolume:
title: 'Are you sure that you want to detach volume {name}?'
actionLabel: Detach Volume
success: 'Volume { name } is detached successfully.'
detachNIC:
title: 'Are you sure that you want to detach network interface {name}?'
actionLabel: Detach Network Interface
success: 'The settings have been saved, but the network interface {name} will be detached only after the virtual machine is migrated.'
instance: instance:
singleInstance: singleInstance:
multipleInstance: multipleInstance:
@ -613,11 +643,6 @@ harvester:
title: 'Select the volume you want to delete:' title: 'Select the volume you want to delete:'
deleteAll: Delete All deleteAll: Delete All
tips: "Warn: The snapshots of the virtual machine will be deleted with virtual machine and the snapshots of volume will be deleted with volume." tips: "Warn: The snapshots of the virtual machine will be deleted with virtual machine and the snapshots of volume will be deleted with volume."
unplug:
title: 'Are you sure that you want to detach volume {name} ?'
actionLabel: Detach
detachVolume:
Detach Volume
restartTip: |- restartTip: |-
{restart, select, {restart, select,
true {Restart} true {Restart}
@ -897,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
@ -1064,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.
@ -1179,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.'
@ -1534,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
@ -1695,7 +1725,7 @@ harvester:
available: Available available: Available
total: Total total: Total
vGPUID: vGPU ID vGPUID: vGPU ID
goSriovGPU: goSriovGPU:
prefix: Please enable the supported GPU devices in prefix: Please enable the supported GPU devices in
middle: SR-IOV GPU Devices middle: SR-IOV GPU Devices
suffix: page to manage the vGPU MIG configurations. suffix: page to manage the vGPU MIG configurations.

View File

@ -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;
} }
}, },

View File

@ -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;

View File

@ -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() {

View File

@ -35,15 +35,10 @@ export default class HciPv extends HarvesterResource {
get availableActions() { get availableActions() {
let out = super._availableActions; let out = super._availableActions;
// Longhorn V2 provisioner do not support volume clone feature yet const clone = out.find((action) => action.action === 'goToClone');
if (this.isLonghornV2) {
out = out.filter((action) => action.action !== 'goToClone');
} else {
const clone = out.find((action) => action.action === 'goToClone');
if (clone) { if (clone) {
clone.action = 'goToCloneVolume'; clone.action = 'goToCloneVolume';
}
} }
const exportImageAction = { const exportImageAction = {
@ -65,10 +60,6 @@ export default class HciPv extends HarvesterResource {
takeSnapshotAction, takeSnapshotAction,
...out ...out
]; ];
// TODO: remove this block if Longhorn V2 engine supports restore volume snapshot
if (this.isLonghornV2) {
out = out.filter((action) => action.action !== takeSnapshotAction.action);
}
} else { // v1.4 / v1.3 } else { // v1.4 / v1.3
if (!this.isLonghorn || !this.isLonghornV2) { if (!this.isLonghorn || !this.isLonghornV2) {
out = [ out = [

View File

@ -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() {

View File

@ -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 });

View File

@ -29,6 +29,16 @@ export default class NetworkAttachmentDef extends SteveModel {
} }
} }
get isSystem() {
const systemNamespaces = this.$rootGetters['systemNamespaces'];
if (systemNamespaces.includes(this.metadata?.namespace)) {
return true;
}
return false;
}
get isIpamStatic() { get isIpamStatic() {
return this.parseConfig.ipam?.type === 'static'; return this.parseConfig.ipam?.type === 'static';
} }

View File

@ -87,17 +87,11 @@ const IgnoreMessages = ['pod has unbound immediate PersistentVolumeClaims'];
export default class VirtVm extends HarvesterResource { export default class VirtVm extends HarvesterResource {
get availableActions() { get availableActions() {
let out = super._availableActions; const out = super._availableActions;
const clone = out.find((action) => action.action === 'goToClone');
// VM attached with Longhorn V2 volume doesn't support clone feature if (clone) {
if (this.longhornV2Volumes.length > 0) { clone.action = 'goToCloneVM';
out = out.filter((action) => action.action !== 'goToClone');
} else {
const clone = out.find((action) => action.action === 'goToClone');
if (clone) {
clone.action = 'goToCloneVM';
}
} }
return [ return [
@ -159,7 +153,7 @@ export default class VirtVm extends HarvesterResource {
}, },
{ {
action: 'takeVMSnapshot', action: 'takeVMSnapshot',
enabled: (!!this.actions?.snapshot || !!this.action?.backup) && !this.longhornV2Volumes.length, enabled: (!!this.actions?.snapshot || !!this.action?.backup),
icon: 'icon icon-snapshot', icon: 'icon icon-snapshot',
label: this.t('harvester.action.vmSnapshot') label: this.t('harvester.action.vmSnapshot')
}, },
@ -206,10 +200,16 @@ export default class VirtVm extends HarvesterResource {
label: this.t('harvester.action.abortMigration') label: this.t('harvester.action.abortMigration')
}, },
{ {
action: 'addHotplug', action: 'addHotplugVolume',
enabled: !!this.actions?.addVolume, enabled: !!this.actions?.addVolume,
icon: 'icon icon-plus', icon: 'icon icon-plus',
label: this.t('harvester.action.addHotplug') label: this.t('harvester.action.addHotplugVolume')
},
{
action: 'addHotplugNic',
enabled: this.hotplugNicFeatureEnabled && !!this.actions?.addNic,
icon: 'icon icon-plus',
label: this.t('harvester.action.addHotplugNic')
}, },
{ {
action: 'createTemplate', action: 'createTemplate',
@ -395,8 +395,20 @@ export default class VirtVm extends HarvesterResource {
this.$dispatch('promptModal', { this.$dispatch('promptModal', {
resources, resources,
diskName, name: diskName,
component: 'HarvesterUnplugVolume' type: 'volume',
component: 'HarvesterHotUnplug',
});
}
unplugNIC(networkName) {
const resources = this;
this.$dispatch('promptModal', {
resources,
name: networkName,
type: 'network',
component: 'HarvesterHotUnplug',
}); });
} }
@ -504,10 +516,17 @@ export default class VirtVm extends HarvesterResource {
}); });
} }
addHotplug(resources = this) { addHotplugVolume(resources = this) {
this.$dispatch('promptModal', { this.$dispatch('promptModal', {
resources, resources,
component: 'HarvesterAddHotplugModal' component: 'HarvesterAddHotplugVolumeModal'
});
}
addHotplugNic(resources = this) {
this.$dispatch('promptModal', {
resources,
component: 'HarvesterAddHotplugNic'
}); });
} }
@ -1240,6 +1259,10 @@ export default class VirtVm extends HarvesterResource {
return this.$rootGetters['harvester-common/getFeatureEnabled']('vmMachineTypeAuto'); return this.$rootGetters['harvester-common/getFeatureEnabled']('vmMachineTypeAuto');
} }
get hotplugNicFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('hotplugNic');
}
get isBackupTargetUnavailable() { get isBackupTargetUnavailable() {
const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || []; const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || [];
const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target'); const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target');

View File

@ -1,7 +1,7 @@
{ {
"name": "harvester", "name": "harvester",
"description": "Rancher UI Extension for Harvester", "description": "Rancher UI Extension for Harvester",
"version": "1.7.0-dev", "version": "1.7.1",
"private": false, "private": false,
"rancher": { "rancher": {
"annotations": { "annotations": {

View File

@ -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

View File

@ -37,6 +37,7 @@ export const HCI = {
STORAGE: 'harvesterhci.io.storage', STORAGE: 'harvesterhci.io.storage',
RESOURCE_QUOTA: 'harvesterhci.io.resourcequota', RESOURCE_QUOTA: 'harvesterhci.io.resourcequota',
KSTUNED: 'node.harvesterhci.io.ksmtuned', KSTUNED: 'node.harvesterhci.io.ksmtuned',
HUGEPAGES: 'node.harvesterhci.io.hugepage',
PCI_DEVICE: 'devices.harvesterhci.io.pcidevice', PCI_DEVICE: 'devices.harvesterhci.io.pcidevice',
PCI_CLAIM: 'devices.harvesterhci.io.pcideviceclaim', PCI_CLAIM: 'devices.harvesterhci.io.pcideviceclaim',
SR_IOV: 'devices.harvesterhci.io.sriovnetworkdevice', SR_IOV: 'devices.harvesterhci.io.sriovnetworkdevice',

1584
yarn.lock

File diff suppressed because it is too large Load Diff