mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-02-04 06:51:44 +00:00
Compare commits
50 Commits
v1.7.1-dev
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2db7ee7397 | ||
|
|
8b9b5b41b7 | ||
|
|
77599900b5 | ||
|
|
473c1ba355 | ||
|
|
708a95b67b | ||
|
|
fecb3de0cf | ||
|
|
0781bde188 | ||
|
|
0647600e88 | ||
|
|
99dbba7958 | ||
|
|
3dcc50980b | ||
|
|
ee1c3de188 | ||
|
|
915559962a | ||
|
|
b1b1a31c04 | ||
|
|
7f52562d22 | ||
|
|
b140c05697 | ||
|
|
ad9fef63c0 | ||
|
|
786e271ac6 | ||
|
|
c169853e5a | ||
|
|
1352246e1e | ||
|
|
49374bb18a | ||
|
|
fe3a12e28c | ||
|
|
a86302c9d5 | ||
|
|
5fe7e13fcd | ||
|
|
c079984047 | ||
|
|
5769588633 | ||
|
|
b29950f99c | ||
|
|
6c27a46274 | ||
|
|
b03fffbc30 | ||
|
|
5b668a176c | ||
|
|
b4019a2c86 | ||
|
|
416098ffd8 | ||
|
|
3d7b96d86d | ||
|
|
0b37467f76 | ||
|
|
fab7fbec5e | ||
|
|
d94003f8c2 | ||
|
|
dbb5b01cc3 | ||
|
|
467933bda0 | ||
|
|
1b183febdc | ||
|
|
70d3b656f7 | ||
|
|
10d19cd329 | ||
|
|
87e44cb658 | ||
|
|
1715ae754c | ||
|
|
30de2b1a18 | ||
|
|
6fedcc353c | ||
|
|
f9bff21e84 | ||
|
|
6735826e15 | ||
|
|
9e17e239cf | ||
|
|
a1cf41bda9 | ||
|
|
db58024351 | ||
|
|
81bf19419c |
3
.github/renovate.json
vendored
3
.github/renovate.json
vendored
@ -7,7 +7,8 @@
|
|||||||
],
|
],
|
||||||
"baseBranches": [
|
"baseBranches": [
|
||||||
"main",
|
"main",
|
||||||
"/^release-harvester-v\\d+\\.\\d+$/"
|
"release-harvester-v1.7",
|
||||||
|
"release-harvester-v1.8"
|
||||||
],
|
],
|
||||||
"automergeMajor": false,
|
"automergeMajor": false,
|
||||||
"semanticCommits": "enabled",
|
"semanticCommits": "enabled",
|
||||||
|
|||||||
34
.github/workflows/fossa.yml
vendored
Normal file
34
.github/workflows/fossa.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
name: FOSSA Scanning
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main", "release-harvester-v*"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
fossa-scanning:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||||
|
|
||||||
|
# The FOSSA token is shared between all repos in Harvester's GH org. It can
|
||||||
|
# be used directly and there is no need to request specific access to EIO.
|
||||||
|
- name: Read FOSSA token
|
||||||
|
uses: rancher-eio/read-vault-secrets@main
|
||||||
|
with:
|
||||||
|
secrets: |
|
||||||
|
secret/data/github/org/harvester/fossa/credentials token | FOSSA_API_KEY_PUSH_ONLY
|
||||||
|
|
||||||
|
- name: FOSSA scan
|
||||||
|
uses: fossas/fossa-action@main
|
||||||
|
with:
|
||||||
|
api-key: ${{ env.FOSSA_API_KEY_PUSH_ONLY }}
|
||||||
|
# Only runs the scan and do not provide/returns any results back to the
|
||||||
|
# pipeline.
|
||||||
|
run-tests: false
|
||||||
19
.github/workflows/run-lint.yaml
vendored
19
.github/workflows/run-lint.yaml
vendored
@ -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 }}
|
||||||
|
|||||||
@ -163,7 +163,7 @@ If you want to contribute, start by reading this document, then visit our [Getti
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (c) 2014-2025 [SUSE, LLC.](https://www.suse.com/)
|
Copyright (c) 2014-2026 [SUSE, LLC.](https://www.suse.com/)
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
14
package.json
14
package.json
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.7.0-dev",
|
"version": "1.8.0-dev",
|
||||||
"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.9-rc.1",
|
||||||
"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,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"
|
||||||
},
|
},
|
||||||
|
|||||||
178
pkg/harvester/README.md
Normal file
178
pkg/harvester/README.md
Normal 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-2026 [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.
|
||||||
@ -1,6 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import Collapse from '@shell/components/Collapse';
|
import Collapse from '@shell/components/Collapse';
|
||||||
import PercentageBar from '@shell/components/PercentageBar';
|
import PercentageBar from '@shell/components/PercentageBar';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterUpgradeProgressList',
|
name: 'HarvesterUpgradeProgressList',
|
||||||
@ -25,13 +27,45 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
await this.$store.dispatch('harvester/findAll', { type: HCI.UPGRADE });
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return { open: true };
|
return { open: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
showResumeButton() {
|
||||||
|
return this.title === 'Upgrading Node';
|
||||||
|
},
|
||||||
|
latestUpgradeCR() {
|
||||||
|
return this.$store.getters['harvester/all'](HCI.UPGRADE).find( (U) => U.isLatestUpgrade);
|
||||||
|
},
|
||||||
|
resumeUpgradePausedNodeEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('resumeUpgradePausedNode');
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleSwitch() {
|
handleSwitch() {
|
||||||
this.open = !this.open;
|
this.open = !this.open;
|
||||||
|
},
|
||||||
|
async resumeNodeUpgrade(nodeName) {
|
||||||
|
if (!this.latestUpgradeCR || !nodeName) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const upgradePauseMapString = this.latestUpgradeCR.metadata.annotations[HCI_ANNOTATIONS.NODE_UPGRADE_PAUSE_MAP] || '{}';
|
||||||
|
const upgradePauseMap = JSON.parse(upgradePauseMapString);
|
||||||
|
|
||||||
|
// update the upgrade CR annotation harvesterhci.io/node-upgrade-pause-map to unpause the node upgrade process
|
||||||
|
upgradePauseMap[`${ nodeName }`] = 'unpause';
|
||||||
|
this.latestUpgradeCR.setAnnotation(HCI_ANNOTATIONS.NODE_UPGRADE_PAUSE_MAP, JSON.stringify(upgradePauseMap));
|
||||||
|
await this.latestUpgradeCR.save();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`unable to update harvester upgrade CR annotations: ${ this.latestUpgradeCR.id }.`, e); // eslint-disable-line no-console
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -63,12 +97,28 @@ export default {
|
|||||||
v-for="(item, i) in list"
|
v-for="(item, i) in list"
|
||||||
:key="i"
|
:key="i"
|
||||||
>
|
>
|
||||||
<p>
|
<div class="upgrade-node-header">
|
||||||
{{ item.name }} <span
|
<div class="upgrade-node-title">
|
||||||
class="status"
|
<p>
|
||||||
:class="{ [item.state]: true }"
|
{{ item.name }}
|
||||||
>{{ item.state }}</span>
|
</p>
|
||||||
</p>
|
<span
|
||||||
|
class="status"
|
||||||
|
:class="{ [item.state]: true }"
|
||||||
|
>
|
||||||
|
{{ item.state }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="showResumeButton && resumeUpgradePausedNodeEnabled && item.state === 'Node-upgrade paused'"
|
||||||
|
type="button"
|
||||||
|
class="btn bg-info btn-sm"
|
||||||
|
data-testid="add-item"
|
||||||
|
@click="resumeNodeUpgrade(item.name)"
|
||||||
|
>
|
||||||
|
{{ t('action.resume') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<PercentageBar
|
<PercentageBar
|
||||||
:model-value="item.percent"
|
:model-value="item.percent"
|
||||||
preferred-direction="MORE"
|
preferred-direction="MORE"
|
||||||
@ -102,10 +152,21 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.custom-content {
|
.custom-content {
|
||||||
margin-bottom: 14px;
|
.upgrade-node-title {
|
||||||
p {
|
flex: 1 0 80%;
|
||||||
|
margin-right: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.upgrade-node-header {
|
||||||
|
display:flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
margin-bottom: 14px;
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
@ -117,6 +178,8 @@ export default {
|
|||||||
}
|
}
|
||||||
.warning {
|
.warning {
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
|||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { NODE } from '@shell/config/types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterUpgradeConfig',
|
name: 'HarvesterUpgradeConfig',
|
||||||
@ -15,6 +17,13 @@ export default {
|
|||||||
},
|
},
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const hash = { nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }) };
|
||||||
|
|
||||||
|
await allHash(hash);
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
let parseDefaultValue = {};
|
let parseDefaultValue = {};
|
||||||
|
|
||||||
@ -39,7 +48,25 @@ export default {
|
|||||||
{ value: 'skip', label: 'skip' },
|
{ value: 'skip', label: 'skip' },
|
||||||
{ value: 'parallel', label: 'parallel' }
|
{ value: 'parallel', label: 'parallel' }
|
||||||
];
|
];
|
||||||
}
|
},
|
||||||
|
nodeUpgradeOptions() {
|
||||||
|
return [
|
||||||
|
{ value: 'auto', label: 'auto' },
|
||||||
|
{ value: 'manual', label: 'manual' }
|
||||||
|
];
|
||||||
|
},
|
||||||
|
nodesOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const nodes = this.$store.getters[`${ inStore }/all`](NODE);
|
||||||
|
|
||||||
|
return nodes.map((node) => ({ value: node.id, label: node.name }));
|
||||||
|
},
|
||||||
|
showPauseNodes() {
|
||||||
|
return this.parseDefaultValue.nodeUpgradeOption?.strategy?.mode === 'manual';
|
||||||
|
},
|
||||||
|
resumeUpgradePausedNodeEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('resumeUpgradePausedNode');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
@ -48,6 +75,18 @@ export default {
|
|||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
normalizeValue(obj) {
|
normalizeValue(obj) {
|
||||||
|
// handle nodeUpgradeOption.strategy
|
||||||
|
if (obj?.nodeUpgradeOption?.strategy?.mode === 'auto') {
|
||||||
|
delete obj.nodeUpgradeOption.strategy.pauseNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj?.nodeUpgradeOption?.strategy?.mode === 'manual') {
|
||||||
|
if (!Array.isArray(obj.nodeUpgradeOption.strategy.pauseNodes)) {
|
||||||
|
obj.nodeUpgradeOption.strategy.pauseNodes = this.nodesOptions.map((node) => node.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle imagePreloadOption.strategy
|
||||||
if (!obj.imagePreloadOption) {
|
if (!obj.imagePreloadOption) {
|
||||||
obj.imagePreloadOption = { strategy: { type: 'sequential' } };
|
obj.imagePreloadOption = { strategy: { type: 'sequential' } };
|
||||||
}
|
}
|
||||||
@ -105,8 +144,8 @@ export default {
|
|||||||
this.update();
|
this.update();
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -144,6 +183,28 @@ export default {
|
|||||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
|
<div v-if="resumeUpgradePausedNodeEnabled">
|
||||||
|
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.nodeUpgradeOption') }}</b></label>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="parseDefaultValue.nodeUpgradeOption.strategy.mode"
|
||||||
|
class="mb-20 label-select"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.setting.upgrade.strategy')"
|
||||||
|
:options="nodeUpgradeOptions"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
<LabeledSelect
|
||||||
|
v-if="showPauseNodes"
|
||||||
|
v-model:value="parseDefaultValue.nodeUpgradeOption.strategy.pauseNodes"
|
||||||
|
class="mb-20 label-select"
|
||||||
|
:clearable="true"
|
||||||
|
:multiple="true"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.setting.upgrade.pauseNodes')"
|
||||||
|
:options="nodesOptions"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="errors.length"
|
v-if="errors.length"
|
||||||
class="error"
|
class="error"
|
||||||
|
|||||||
@ -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`,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -53,8 +53,12 @@ const FEATURE_FLAGS = {
|
|||||||
'vmMachineTypeAuto',
|
'vmMachineTypeAuto',
|
||||||
'lhV2VolExpansion',
|
'lhV2VolExpansion',
|
||||||
'l2VlanTrunkMode',
|
'l2VlanTrunkMode',
|
||||||
'kubevirtMigration'
|
'kubevirtMigration',
|
||||||
]
|
'hotplugNic',
|
||||||
|
'resumeUpgradePausedNode',
|
||||||
|
],
|
||||||
|
'v1.7.1': [],
|
||||||
|
'v1.8.0': []
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateFeatureFlags = () => {
|
const generateFeatureFlags = () => {
|
||||||
|
|||||||
@ -35,8 +35,21 @@ import {
|
|||||||
SNAPSHOT_TARGET_VOLUME,
|
SNAPSHOT_TARGET_VOLUME,
|
||||||
IMAGE_VIRTUAL_SIZE,
|
IMAGE_VIRTUAL_SIZE,
|
||||||
IMAGE_STORAGE_CLASS,
|
IMAGE_STORAGE_CLASS,
|
||||||
HARVESTER_DESCRIPTION
|
HARVESTER_DESCRIPTION,
|
||||||
|
VM_IMPORT_SOURCE_VM,
|
||||||
|
VM_IMPORT_SOURCE_CLUSTER,
|
||||||
|
VM_IMPORT_STATUS,
|
||||||
|
VM_IMPORT_SOURCE_V_DC,
|
||||||
|
VM_IMPORT_SOURCE_V_ENDPOINT,
|
||||||
|
VM_IMPORT_SOURCE_V_STATUS,
|
||||||
|
VM_IMPORT_SOURCE_O_REGION,
|
||||||
|
VM_IMPORT_SOURCE_O_ENDPOINT,
|
||||||
|
VM_IMPORT_SOURCE_O_STATUS,
|
||||||
|
VM_IMPORT_SOURCE_OVA_URL,
|
||||||
|
VM_IMPORT_SOURCE_OVA_STATUS,
|
||||||
} from './table-headers';
|
} from './table-headers';
|
||||||
|
import { ADD_ONS } from './harvester-map';
|
||||||
|
import { registerAddonSideNav } from '../utils/dynamic-nav';
|
||||||
|
|
||||||
const TEMPLATE = HCI.VM_VERSION;
|
const TEMPLATE = HCI.VM_VERSION;
|
||||||
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
|
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
|
||||||
@ -195,6 +208,142 @@ export function init($plugin, store) {
|
|||||||
exact: false
|
exact: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// VM Import Controller UI Flow
|
||||||
|
// ===========================================================================
|
||||||
|
// Define group (Hidden by default)
|
||||||
|
weightGroup('vmimport', 0, false);
|
||||||
|
|
||||||
|
// VirtualMachineImport
|
||||||
|
headers(HCI.VMIMPORT, [
|
||||||
|
STATE,
|
||||||
|
NAME_COL,
|
||||||
|
NAMESPACE_COL,
|
||||||
|
VM_IMPORT_SOURCE_VM,
|
||||||
|
VM_IMPORT_SOURCE_CLUSTER,
|
||||||
|
VM_IMPORT_STATUS,
|
||||||
|
AGE
|
||||||
|
]);
|
||||||
|
configureType(HCI.VMIMPORT, {
|
||||||
|
resource: HCI.VMIMPORT,
|
||||||
|
resourceDetail: HCI.VMIMPORT,
|
||||||
|
resourceEdit: HCI.VMIMPORT,
|
||||||
|
location: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VMIMPORT }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
|
||||||
|
name: HCI.VMIMPORT,
|
||||||
|
labelKey: 'harvester.addons.vmImport.labels.vmimport',
|
||||||
|
group: 'vmimport',
|
||||||
|
namespaced: true,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VMIMPORT }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Source: VMware
|
||||||
|
headers(HCI.VMIMPORT_SOURCE_V, [
|
||||||
|
STATE,
|
||||||
|
NAME_COL,
|
||||||
|
VM_IMPORT_SOURCE_V_ENDPOINT,
|
||||||
|
VM_IMPORT_SOURCE_V_DC,
|
||||||
|
VM_IMPORT_SOURCE_V_STATUS,
|
||||||
|
AGE
|
||||||
|
]);
|
||||||
|
configureType(HCI.VMIMPORT_SOURCE_V, {
|
||||||
|
resource: HCI.VMIMPORT_SOURCE_V,
|
||||||
|
resourceDetail: HCI.VMIMPORT_SOURCE_V,
|
||||||
|
resourceEdit: HCI.VMIMPORT_SOURCE_V,
|
||||||
|
location: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VMIMPORT_SOURCE_V }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
|
||||||
|
name: HCI.VMIMPORT_SOURCE_V,
|
||||||
|
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare',
|
||||||
|
group: 'vmimport',
|
||||||
|
namespaced: true,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VMIMPORT_SOURCE_V }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Source: OpenStack
|
||||||
|
headers(HCI.VMIMPORT_SOURCE_O, [
|
||||||
|
STATE,
|
||||||
|
NAME_COL,
|
||||||
|
VM_IMPORT_SOURCE_O_ENDPOINT,
|
||||||
|
VM_IMPORT_SOURCE_O_REGION,
|
||||||
|
VM_IMPORT_SOURCE_O_STATUS,
|
||||||
|
AGE
|
||||||
|
]);
|
||||||
|
configureType(HCI.VMIMPORT_SOURCE_O, {
|
||||||
|
resource: HCI.VMIMPORT_SOURCE_O,
|
||||||
|
resourceDetail: HCI.VMIMPORT_SOURCE_O,
|
||||||
|
resourceEdit: HCI.VMIMPORT_SOURCE_O,
|
||||||
|
location: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VMIMPORT_SOURCE_O }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
|
||||||
|
name: HCI.VMIMPORT_SOURCE_O,
|
||||||
|
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack',
|
||||||
|
group: 'vmimport',
|
||||||
|
namespaced: true,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VMIMPORT_SOURCE_O }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Source: OVA
|
||||||
|
headers(HCI.VMIMPORT_SOURCE_OVA, [
|
||||||
|
STATE,
|
||||||
|
NAME_COL,
|
||||||
|
VM_IMPORT_SOURCE_OVA_URL,
|
||||||
|
VM_IMPORT_SOURCE_OVA_STATUS,
|
||||||
|
AGE
|
||||||
|
]);
|
||||||
|
configureType(HCI.VMIMPORT_SOURCE_OVA, {
|
||||||
|
resource: HCI.VMIMPORT_SOURCE_OVA,
|
||||||
|
resourceDetail: HCI.VMIMPORT_SOURCE_OVA,
|
||||||
|
resourceEdit: HCI.VMIMPORT_SOURCE_OVA,
|
||||||
|
location: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VMIMPORT_SOURCE_OVA }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
|
||||||
|
name: HCI.VMIMPORT_SOURCE_OVA,
|
||||||
|
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA',
|
||||||
|
group: 'vmimport',
|
||||||
|
namespaced: true,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VMIMPORT_SOURCE_OVA }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable SideNav based on Addon Status
|
||||||
|
registerAddonSideNav(store, PRODUCT_NAME, {
|
||||||
|
addonName: ADD_ONS.VM_IMPORT_CONTROLLER,
|
||||||
|
resourceType: HCI.ADD_ONS,
|
||||||
|
navGroup: 'vmimport',
|
||||||
|
types: [
|
||||||
|
HCI.VMIMPORT_SOURCE_V,
|
||||||
|
HCI.VMIMPORT_SOURCE_O,
|
||||||
|
HCI.VMIMPORT_SOURCE_OVA,
|
||||||
|
HCI.VMIMPORT
|
||||||
|
]
|
||||||
|
});
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
basicType([HCI.VOLUME]);
|
basicType([HCI.VOLUME]);
|
||||||
configureType(HCI.VOLUME, {
|
configureType(HCI.VOLUME, {
|
||||||
location: {
|
location: {
|
||||||
|
|||||||
@ -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,6 @@ 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',
|
||||||
|
NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -131,3 +131,102 @@ export const PROVIDER = {
|
|||||||
value: 'spec.provider',
|
value: 'spec.provider',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Source VM column in migration.harvesterhci.io.virtualmachineimport list page
|
||||||
|
export const VM_IMPORT_SOURCE_VM = {
|
||||||
|
name: 'sourceVm',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportSourceVm',
|
||||||
|
value: 'spec.virtualMachineName',
|
||||||
|
sort: 'spec.virtualMachineName',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Source Cluster column in migration.harvesterhci.io.virtualmachineimport list page
|
||||||
|
export const VM_IMPORT_SOURCE_CLUSTER = {
|
||||||
|
name: 'sourceCluster',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportSourceCluster',
|
||||||
|
value: 'spec.sourceCluster.name',
|
||||||
|
sort: 'spec.sourceCluster.name',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Import Status column in migration.harvesterhci.io.virtualmachineimport list page
|
||||||
|
export const VM_IMPORT_STATUS = {
|
||||||
|
name: 'importStatus',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportStatus',
|
||||||
|
value: 'status.importStatus',
|
||||||
|
sort: 'status.importStatus',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Datacenter column in migration.harvesterhci.io.vmwaresource list page
|
||||||
|
export const VM_IMPORT_SOURCE_V_DC = {
|
||||||
|
name: 'datacenter',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportSourceVDatacenter',
|
||||||
|
value: 'spec.dc',
|
||||||
|
sort: 'spec.dc',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Endpoint column in migration.harvesterhci.io.vmwaresource list page
|
||||||
|
export const VM_IMPORT_SOURCE_V_ENDPOINT = {
|
||||||
|
name: 'endpoint',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportSourceVEndpoint',
|
||||||
|
value: 'spec.endpoint',
|
||||||
|
sort: 'spec.endpoint',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cluster Status column in migration.harvesterhci.io.vmwaresource list page
|
||||||
|
export const VM_IMPORT_SOURCE_V_STATUS = {
|
||||||
|
name: 'clusterStatus',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportSourceVClusterStatus',
|
||||||
|
value: 'status.status',
|
||||||
|
sort: 'status.status',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Region column in migration.harvesterhci.io.openstacksource list page
|
||||||
|
export const VM_IMPORT_SOURCE_O_REGION = {
|
||||||
|
name: 'region',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportSourceORegion',
|
||||||
|
value: 'spec.region',
|
||||||
|
sort: 'spec.region',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Endpoint column in migration.harvesterhci.io.openstacksource list page
|
||||||
|
export const VM_IMPORT_SOURCE_O_ENDPOINT = {
|
||||||
|
name: 'endpoint',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportSourceOEndpoint',
|
||||||
|
value: 'spec.endpoint',
|
||||||
|
sort: 'spec.endpoint',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cluster Status column in migration.harvesterhci.io.openstacksource list page
|
||||||
|
export const VM_IMPORT_SOURCE_O_STATUS = {
|
||||||
|
name: 'clusterStatus',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportSourceOClusterStatus',
|
||||||
|
value: 'status.status',
|
||||||
|
sort: 'status.status',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// URL column in migration.harvesterhci.io.ovasource list page
|
||||||
|
export const VM_IMPORT_SOURCE_OVA_URL = {
|
||||||
|
name: 'url',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportSourceOVAUrl',
|
||||||
|
value: 'spec.url',
|
||||||
|
sort: 'spec.url',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Status column in migration.harvesterhci.io.ovasource list page
|
||||||
|
export const VM_IMPORT_SOURCE_OVA_STATUS = {
|
||||||
|
name: 'status',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportSourceOVAStatus',
|
||||||
|
value: 'status.status',
|
||||||
|
sort: 'status.status',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|||||||
@ -29,3 +29,15 @@ export const L2VLAN_MODE = {
|
|||||||
ACCESS: 'access',
|
ACCESS: 'access',
|
||||||
TRUNK: 'trunk',
|
TRUNK: 'trunk',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const VMIMPORT_SOURCE_PROVIDER = {
|
||||||
|
VMWARE: 'vmware',
|
||||||
|
OPENSTACK: 'openstack',
|
||||||
|
OVA: 'ova',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VMIMPORT_SOURCE_KINDS = {
|
||||||
|
VMWARE: 'VmwareSource',
|
||||||
|
OPENSTACK: 'OpenstackSource',
|
||||||
|
OVA: 'OvaSource',
|
||||||
|
};
|
||||||
|
|||||||
119
pkg/harvester/detail/harvesterhci.io.host/HarvesterHugepages.vue
Normal file
119
pkg/harvester/detail/harvesterhci.io.host/HarvesterHugepages.vue
Normal 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>
|
||||||
@ -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"
|
||||||
|
|||||||
@ -75,7 +75,7 @@ export default {
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col span-6 mb-20">
|
<div class="col span-6 mb-20">
|
||||||
<LabelValue
|
<LabelValue
|
||||||
:name="t('harvester.schedule.cron')"
|
:name="t('harvester.schedule.cron.label')"
|
||||||
:value="cronExpression"
|
:value="cronExpression"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
212
pkg/harvester/dialog/HarvesterAddHotplugNic.vue
Normal file
212
pkg/harvester/dialog/HarvesterAddHotplugNic.vue
Normal 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>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -277,8 +277,9 @@ export default {
|
|||||||
:label="t('harvester.modal.bundle.namespaces.label')"
|
:label="t('harvester.modal.bundle.namespaces.label')"
|
||||||
:clearable="true"
|
:clearable="true"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
|
:append-to-body="false"
|
||||||
:options="namespaceOptions"
|
:options="namespaceOptions"
|
||||||
class="mb-10 label-select"
|
class="mb-10 namespace-select"
|
||||||
:tooltip="t('harvester.modal.bundle.namespaces.tooltip', _, true)"
|
:tooltip="t('harvester.modal.bundle.namespaces.tooltip', _, true)"
|
||||||
@update:value="updateNamespaces"
|
@update:value="updateNamespaces"
|
||||||
/>
|
/>
|
||||||
@ -372,6 +373,11 @@ export default {
|
|||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
height: 160px;
|
height: 160px;
|
||||||
}
|
}
|
||||||
|
.namespace-select {
|
||||||
|
:deep(.vs__dropdown-menu) {
|
||||||
|
max-height: 210px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
|
|||||||
157
pkg/harvester/edit/harvesterhci.io.host/HarvesterHugepages.vue
Normal file
157
pkg/harvester/edit/harvesterhci.io.host/HarvesterHugepages.vue
Normal 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>
|
||||||
@ -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"
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
@ -16,6 +17,7 @@ import { sortBy } from '@shell/utils/sort';
|
|||||||
import { BACKUP_TYPE } from '../config/types';
|
import { BACKUP_TYPE } from '../config/types';
|
||||||
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
||||||
import { isBackupTargetSettingEmpty, isBackupTargetSettingUnavailable } from '../utils/setting';
|
import { isBackupTargetSettingEmpty, isBackupTargetSettingUnavailable } from '../utils/setting';
|
||||||
|
import CronExpressionEditorModal from '@shell/components/Cron/CronExpressionEditorModal.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CreateVMSchedule',
|
name: 'CreateVMSchedule',
|
||||||
@ -28,6 +30,7 @@ export default {
|
|||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
MessageLink,
|
MessageLink,
|
||||||
Banner,
|
Banner,
|
||||||
|
CronExpressionEditorModal
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
@ -86,10 +89,12 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { settings: [] };
|
return { settings: [], showModel: false };
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
backupTargetResource() {
|
backupTargetResource() {
|
||||||
return this.settings.find( (O) => O.id === 'backup-target');
|
return this.settings.find( (O) => O.id === 'backup-target');
|
||||||
},
|
},
|
||||||
@ -172,6 +177,9 @@ export default {
|
|||||||
this.value.spec['maxFailure'] = this.value.spec.retain;
|
this.value.spec['maxFailure'] = this.value.spec.retain;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
openModal() {
|
||||||
|
this.showModel = true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -256,16 +264,28 @@ export default {
|
|||||||
:weight="99"
|
:weight="99"
|
||||||
class="bordered-table"
|
class="bordered-table"
|
||||||
>
|
>
|
||||||
<LabeledInput
|
<div class="cronEditor">
|
||||||
v-model:value="value.spec.cron"
|
<LabeledInput
|
||||||
class="mb-30"
|
v-model:value="value.spec.cron"
|
||||||
type="cron"
|
class="mb-30"
|
||||||
required
|
type="cron"
|
||||||
:mode="mode"
|
required
|
||||||
:label="t('harvester.schedule.cron')"
|
:mode="mode"
|
||||||
placeholder="0 * * * *"
|
:label="t('harvester.schedule.cron.label')"
|
||||||
:disabled="isBackupTargetUnAvailable || isView"
|
placeholder="0 * * * *"
|
||||||
/>
|
:disabled="isBackupTargetUnAvailable || isView"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="editCronBtn btn role-primary"
|
||||||
|
@click="openModal"
|
||||||
|
>
|
||||||
|
{{ t('harvester.schedule.cron.editButton') }}
|
||||||
|
</button>
|
||||||
|
<CronExpressionEditorModal
|
||||||
|
v-model:show="showModel"
|
||||||
|
v-model:cron-expression="value.spec.cron"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-model:value.number="value.spec.retain"
|
v-model:value.number="value.spec.retain"
|
||||||
class="mb-30"
|
class="mb-30"
|
||||||
@ -292,3 +312,16 @@ export default {
|
|||||||
</Tabbed>
|
</Tabbed>
|
||||||
</CruResource>
|
</CruResource>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.cronEditor {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editCronBtn {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
margin-left: 10px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -54,6 +54,11 @@ export default {
|
|||||||
|
|
||||||
const vmDevices = this.value?.domain?.devices?.hostDevices || [];
|
const vmDevices = this.value?.domain?.devices?.hostDevices || [];
|
||||||
const otherDevices = this.otherDevices(vmDevices).map(({ name }) => name);
|
const otherDevices = this.otherDevices(vmDevices).map(({ name }) => name);
|
||||||
|
const vmDeviceNames = vmDevices.map(({ name }) => name);
|
||||||
|
|
||||||
|
this.pciDevices.forEach((row) => {
|
||||||
|
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
|
||||||
|
});
|
||||||
|
|
||||||
vmDevices.forEach(({ name, deviceName }) => {
|
vmDevices.forEach(({ name, deviceName }) => {
|
||||||
const checkName = (deviceName || '').split('/')?.[1];
|
const checkName = (deviceName || '').split('/')?.[1];
|
||||||
|
|||||||
@ -48,6 +48,13 @@ export default {
|
|||||||
this[key] = res[key];
|
this[key] = res[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const vmDevices = this.value?.domain?.devices?.hostDevices || [];
|
||||||
|
const vmDeviceNames = vmDevices.map(({ name }) => name);
|
||||||
|
|
||||||
|
this.devices.forEach((row) => {
|
||||||
|
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
|
||||||
|
});
|
||||||
|
|
||||||
this.selectedDevices = (this.value?.domain?.devices?.hostDevices || [])
|
this.selectedDevices = (this.value?.domain?.devices?.hostDevices || [])
|
||||||
.map(({ name }) => name)
|
.map(({ name }) => name)
|
||||||
.filter((name) => this.enabledDevices.find((device) => device?.metadata?.name === name));
|
.filter((name) => this.enabledDevices.find((device) => device?.metadata?.name === name));
|
||||||
|
|||||||
@ -46,6 +46,13 @@ export default {
|
|||||||
this[key] = res[key];
|
this[key] = res[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const vmDevices = this.value?.domain?.devices?.gpus || [];
|
||||||
|
const vmDeviceNames = vmDevices.map(({ name }) => name);
|
||||||
|
|
||||||
|
this.devices.forEach((row) => {
|
||||||
|
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
|
||||||
|
});
|
||||||
|
|
||||||
const vGpus = this.vm.isOff ? [
|
const vGpus = this.vm.isOff ? [
|
||||||
...(this.value?.domain?.devices?.gpus || []).map(({ name }) => name),
|
...(this.value?.domain?.devices?.gpus || []).map(({ name }) => name),
|
||||||
] : [
|
] : [
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
357
pkg/harvester/edit/migration.harvesterhci.io.openstacksource.vue
Normal file
357
pkg/harvester/edit/migration.harvesterhci.io.openstacksource.vue
Normal file
@ -0,0 +1,357 @@
|
|||||||
|
<script>
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import FormValidation from '@shell/mixins/form-validation';
|
||||||
|
import { SECRET } from '@shell/config/types';
|
||||||
|
import { randomStr } from '@shell/utils/string';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditOpenstackSource',
|
||||||
|
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
CruResource,
|
||||||
|
Tabbed,
|
||||||
|
Tab,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
NameNsDescription,
|
||||||
|
RadioGroup,
|
||||||
|
UnitInput
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView, FormValidation],
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
this.allSecrets = await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
if (!this.value.spec) this.value.spec = {};
|
||||||
|
if (!this.value.spec.credentials) this.value.spec.credentials = {};
|
||||||
|
|
||||||
|
const initialMode = this.value.spec.credentials.name ? 'existing' : 'new';
|
||||||
|
|
||||||
|
return {
|
||||||
|
allSecrets: [],
|
||||||
|
authMode: initialMode,
|
||||||
|
|
||||||
|
newUsername: '',
|
||||||
|
newPassword: '',
|
||||||
|
newProjectName: '',
|
||||||
|
newDomainName: '',
|
||||||
|
newCaCert: '',
|
||||||
|
|
||||||
|
// Rules for fields that exist in the value object (Model)
|
||||||
|
fvFormRuleSets: [
|
||||||
|
{ path: 'metadata.name', rules: ['nameRequired'] },
|
||||||
|
{ path: 'spec.endpoint', rules: ['endpointRequired'] },
|
||||||
|
{ path: 'spec.region', rules: ['regionRequired'] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
authModeOptions() {
|
||||||
|
return [
|
||||||
|
{ label: this.t('harvester.addons.vmImport.fields.createSecret'), value: 'new' },
|
||||||
|
{ label: this.t('harvester.addons.vmImport.fields.useSecret'), value: 'existing' }
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
secretOptions() {
|
||||||
|
const currentNamespace = this.value.metadata.namespace || 'default';
|
||||||
|
|
||||||
|
return this.allSecrets
|
||||||
|
.filter((s) => s.metadata.namespace === currentNamespace)
|
||||||
|
.map((s) => ({
|
||||||
|
label: s.nameDisplay,
|
||||||
|
value: s.metadata.name
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
// Define custom rules for the FormValidation mixin
|
||||||
|
fvExtraRules() {
|
||||||
|
return {
|
||||||
|
nameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.fields.name') }) : undefined,
|
||||||
|
endpointRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.openstack.fields.endpoint') }) : undefined,
|
||||||
|
regionRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.openstack.fields.region') }) : undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Combine mixin validation + conditional manual checks
|
||||||
|
isFormValid() {
|
||||||
|
// Check static fields via Mixin
|
||||||
|
if (!this.fvFormIsValid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check conditional fields
|
||||||
|
if (this.authMode === 'new') {
|
||||||
|
if (!this.newUsername || !this.newPassword) return false;
|
||||||
|
if (!this.newProjectName || !this.newDomainName) return false;
|
||||||
|
} else {
|
||||||
|
if (!this.value.spec.credentials.name) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
usernameRule(val) {
|
||||||
|
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.username') }) : undefined;
|
||||||
|
},
|
||||||
|
passwordRule(val) {
|
||||||
|
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.password') }) : undefined;
|
||||||
|
},
|
||||||
|
projectRule(val) {
|
||||||
|
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.openstack.fields.projectName') }) : undefined;
|
||||||
|
},
|
||||||
|
domainRule(val) {
|
||||||
|
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.openstack.fields.domainName') }) : undefined;
|
||||||
|
},
|
||||||
|
secretRule(val) {
|
||||||
|
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.selectSecret') }) : undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveSource(buttonCb) {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.authMode === 'new') {
|
||||||
|
const secretName = `${ this.value.metadata.name }-creds-${ randomStr(4).toLowerCase() }`;
|
||||||
|
const namespace = this.value.metadata.namespace || 'default';
|
||||||
|
|
||||||
|
const newSecret = await this.$store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: SECRET,
|
||||||
|
metadata: {
|
||||||
|
name: secretName,
|
||||||
|
namespace
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
newSecret['_type'] = 'Opaque';
|
||||||
|
newSecret['data'] = {
|
||||||
|
username: btoa(this.newUsername),
|
||||||
|
password: btoa(this.newPassword),
|
||||||
|
project_name: btoa(this.newProjectName),
|
||||||
|
domain_name: btoa(this.newDomainName),
|
||||||
|
ca_cert: this.newCaCert ? btoa(this.newCaCert) : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
await newSecret.save();
|
||||||
|
|
||||||
|
this.value.spec.credentials = {
|
||||||
|
name: secretName,
|
||||||
|
namespace
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.save(buttonCb);
|
||||||
|
} catch (err) {
|
||||||
|
this.errors = [err];
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:errors="errors"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
:validation-passed="isFormValid"
|
||||||
|
@finish="saveSource"
|
||||||
|
@error="e=>errors=e"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="{ name: fvGetAndReportPathRules('metadata.name') }"
|
||||||
|
@update:value="$emit('update:value', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tabbed
|
||||||
|
v-bind="$attrs"
|
||||||
|
class="mt-15"
|
||||||
|
:side-tabs="true"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="basic"
|
||||||
|
:label="t('harvester.addons.vmImport.titles.basic')"
|
||||||
|
:weight="3"
|
||||||
|
>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="value.spec.endpoint"
|
||||||
|
:label="t('harvester.addons.vmImport.openstack.fields.endpoint')"
|
||||||
|
:placeholder="t('harvester.addons.vmImport.openstack.placeholders.endpoint')"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="fvGetAndReportPathRules('spec.endpoint')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="value.spec.region"
|
||||||
|
:label="t('harvester.addons.vmImport.openstack.fields.region')"
|
||||||
|
:placeholder="t('harvester.addons.vmImport.openstack.placeholders.region')"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="fvGetAndReportPathRules('spec.region')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
name="auth"
|
||||||
|
:label="t('harvester.addons.vmImport.titles.auth')"
|
||||||
|
:weight="2"
|
||||||
|
>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="authMode"
|
||||||
|
name="authMode"
|
||||||
|
:options="authModeOptions"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="authMode === 'new'">
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="newUsername"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.username')"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="[usernameRule]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="newPassword"
|
||||||
|
type="password"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.password')"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="[passwordRule]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="newProjectName"
|
||||||
|
:label="t('harvester.addons.vmImport.openstack.fields.projectName')"
|
||||||
|
:placeholder="t('harvester.addons.vmImport.openstack.placeholders.projectName')"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="[projectRule]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="newDomainName"
|
||||||
|
:label="t('harvester.addons.vmImport.openstack.fields.domainName')"
|
||||||
|
:placeholder="t('harvester.addons.vmImport.openstack.placeholders.domainName')"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="[domainRule]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="newCaCert"
|
||||||
|
type="multiline"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.caCert')"
|
||||||
|
:placeholder="t('harvester.addons.vmImport.placeholders.caCert')"
|
||||||
|
:min-height="100"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="authMode === 'existing'">
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="value.spec.credentials.name"
|
||||||
|
:options="secretOptions"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.selectSecret')"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="[secretRule]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
name="advanced"
|
||||||
|
:label="t('harvester.addons.vmImport.titles.advanced')"
|
||||||
|
:weight="1"
|
||||||
|
>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="value.spec.uploadImageRetryCount"
|
||||||
|
:label="t('harvester.addons.vmImport.openstack.fields.retryCount')"
|
||||||
|
:placeholder="t('harvester.addons.vmImport.openstack.placeholders.retryCount')"
|
||||||
|
suffix="Times"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="value.spec.uploadImageRetryDelay"
|
||||||
|
:label="t('harvester.addons.vmImport.openstack.fields.retryDelay')"
|
||||||
|
:placeholder="t('harvester.addons.vmImport.openstack.placeholders.retryDelay')"
|
||||||
|
suffix="Seconds"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
322
pkg/harvester/edit/migration.harvesterhci.io.ovasource.vue
Normal file
322
pkg/harvester/edit/migration.harvesterhci.io.ovasource.vue
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
<script>
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import FormValidation from '@shell/mixins/form-validation';
|
||||||
|
import { SECRET } from '@shell/config/types';
|
||||||
|
import { randomStr } from '@shell/utils/string';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditOvaSource',
|
||||||
|
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
CruResource,
|
||||||
|
Tabbed,
|
||||||
|
Tab,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
NameNsDescription,
|
||||||
|
RadioGroup,
|
||||||
|
UnitInput
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView, FormValidation],
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
this.allSecrets = await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
if (!this.value.spec) this.value.spec = {};
|
||||||
|
|
||||||
|
// Auth is optional for OVA (public URLs).
|
||||||
|
// If credentials.name exists -> Existing.
|
||||||
|
// If not -> None (default).
|
||||||
|
let initialMode = 'none';
|
||||||
|
|
||||||
|
if (this.value.spec.credentials?.name) {
|
||||||
|
initialMode = 'existing';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allSecrets: [],
|
||||||
|
authMode: initialMode,
|
||||||
|
|
||||||
|
newUsername: '',
|
||||||
|
newPassword: '',
|
||||||
|
newCaCert: '', // Key will be "ca.crt"
|
||||||
|
|
||||||
|
// Validation Rules for static fields
|
||||||
|
fvFormRuleSets: [
|
||||||
|
{ path: 'metadata.name', rules: ['nameRequired'] },
|
||||||
|
{ path: 'spec.url', rules: ['urlRequired'] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
authModeOptions() {
|
||||||
|
return [
|
||||||
|
{ label: this.t('harvester.addons.vmImport.fields.none'), value: 'none' },
|
||||||
|
{ label: this.t('harvester.addons.vmImport.fields.createSecret'), value: 'new' },
|
||||||
|
{ label: this.t('harvester.addons.vmImport.fields.useSecret'), value: 'existing' }
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
secretOptions() {
|
||||||
|
const currentNamespace = this.value.metadata.namespace || 'default';
|
||||||
|
|
||||||
|
return this.allSecrets
|
||||||
|
.filter((s) => s.metadata.namespace === currentNamespace)
|
||||||
|
.map((s) => ({
|
||||||
|
label: s.nameDisplay,
|
||||||
|
value: s.metadata.name
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
// Define custom rules for the FormValidation mixin
|
||||||
|
fvExtraRules() {
|
||||||
|
return {
|
||||||
|
nameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.fields.name') }) : undefined,
|
||||||
|
urlRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.ova.fields.url') }) : undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
isFormValid() {
|
||||||
|
if (!this.fvFormIsValid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.authMode === 'new') {
|
||||||
|
// At least a username/password OR a CA cert to be provided.
|
||||||
|
// If the user selected "Create New", they likely intend to enter something.
|
||||||
|
if (!this.newUsername && !this.newPassword && !this.newCaCert) return false;
|
||||||
|
} else if (this.authMode === 'existing') {
|
||||||
|
if (!this.value.spec.credentials?.name) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
authMode(newMode) {
|
||||||
|
if (newMode === 'existing') {
|
||||||
|
// Bind to value.spec.credentials.name for existing credential
|
||||||
|
// Ensure 'credentials' object exists first when selected
|
||||||
|
if (!this.value.spec.credentials) {
|
||||||
|
this.value.spec.credentials = {
|
||||||
|
name: '',
|
||||||
|
namespace: this.value.metadata.namespace || 'default'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
secretRule(val) {
|
||||||
|
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.selectSecret') }) : undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveSource(buttonCb) {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.authMode === 'none') {
|
||||||
|
// Clear any credential reference
|
||||||
|
delete this.value.spec.credentials;
|
||||||
|
} else if (this.authMode === 'new') {
|
||||||
|
const secretName = `${ this.value.metadata.name }-creds-${ randomStr(4).toLowerCase() }`;
|
||||||
|
const namespace = this.value.metadata.namespace || 'default';
|
||||||
|
|
||||||
|
const newSecret = await this.$store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: SECRET,
|
||||||
|
metadata: {
|
||||||
|
name: secretName,
|
||||||
|
namespace
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
newSecret['_type'] = 'Opaque';
|
||||||
|
newSecret['data'] = {
|
||||||
|
// Optional fields logic
|
||||||
|
username: this.newUsername ? btoa(this.newUsername) : undefined,
|
||||||
|
password: this.newPassword ? btoa(this.newPassword) : undefined,
|
||||||
|
// vm-import-controller code specifies "ca.crt" with a dot.
|
||||||
|
'ca.crt': this.newCaCert ? btoa(this.newCaCert) : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
await newSecret.save();
|
||||||
|
|
||||||
|
this.value.spec.credentials = {
|
||||||
|
name: secretName,
|
||||||
|
namespace
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.save(buttonCb);
|
||||||
|
} catch (err) {
|
||||||
|
this.errors = [err];
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:errors="errors"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
:validation-passed="isFormValid"
|
||||||
|
@finish="saveSource"
|
||||||
|
@error="e=>errors=e"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="{ name: fvGetAndReportPathRules('metadata.name') }"
|
||||||
|
@update:value="$emit('update:value', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tabbed
|
||||||
|
v-bind="$attrs"
|
||||||
|
class="mt-15"
|
||||||
|
:side-tabs="true"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="basic"
|
||||||
|
:label="t('harvester.addons.vmImport.titles.basic')"
|
||||||
|
:weight="3"
|
||||||
|
>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="value.spec.url"
|
||||||
|
:label="t('harvester.addons.vmImport.ova.fields.url')"
|
||||||
|
:placeholder="t('harvester.addons.vmImport.ova.placeholders.url')"
|
||||||
|
tooltip="Supports HTTP and HTTPS protocols."
|
||||||
|
:mode="mode"
|
||||||
|
:rules="fvGetAndReportPathRules('spec.url')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
name="auth"
|
||||||
|
:label="t('harvester.addons.vmImport.titles.auth')"
|
||||||
|
:weight="2"
|
||||||
|
>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="authMode"
|
||||||
|
name="authMode"
|
||||||
|
:options="authModeOptions"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="authMode === 'new'">
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="newUsername"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.username')"
|
||||||
|
placeholder="(Optional)"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="newPassword"
|
||||||
|
type="password"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.password')"
|
||||||
|
placeholder="(Optional)"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="newCaCert"
|
||||||
|
type="multiline"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.caCert')"
|
||||||
|
:placeholder="t('harvester.addons.vmImport.placeholders.caCert')"
|
||||||
|
:min-height="100"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="authMode === 'existing'">
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="value.spec.credentials.name"
|
||||||
|
:options="secretOptions"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.selectSecret')"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="[secretRule]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
name="advanced"
|
||||||
|
:label="t('harvester.addons.vmImport.titles.advanced')"
|
||||||
|
:weight="1"
|
||||||
|
>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="value.spec.httpTimeoutSeconds"
|
||||||
|
:label="t('harvester.addons.vmImport.ova.fields.httpTimeout')"
|
||||||
|
placeholder="Default: 600"
|
||||||
|
suffix="Seconds"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,570 @@
|
|||||||
|
<script>
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import FormValidation from '@shell/mixins/form-validation';
|
||||||
|
import { STORAGE_CLASS, NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { MANAGEMENT_NETWORK } from '../mixins/harvester-vm';
|
||||||
|
import { VMIMPORT_SOURCE_PROVIDER, VMIMPORT_SOURCE_KINDS } from '../config/types';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import { isValidDNSLabelName } from '@pkg/utils/regular';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
// Full API types for the fetch dispatch
|
||||||
|
const VMWARE_SOURCE_TYPE = `${ HCI.MIGRATION }.${ VMIMPORT_SOURCE_KINDS.VMWARE.toLowerCase() }`;
|
||||||
|
const OPENSTACK_SOURCE_TYPE = `${ HCI.MIGRATION }.${ VMIMPORT_SOURCE_KINDS.OPENSTACK.toLowerCase() }`;
|
||||||
|
const OVA_SOURCE_TYPE = `${ HCI.MIGRATION }.${ VMIMPORT_SOURCE_KINDS.OVA.toLowerCase() }`;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditVirtualMachineImport',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
CruResource,
|
||||||
|
Tabbed,
|
||||||
|
Tab,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
NameNsDescription,
|
||||||
|
Checkbox
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView, FormValidation],
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
// Fetch all dependencies in parallel to speed up the page load
|
||||||
|
const hash = {
|
||||||
|
storageClasses: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
||||||
|
networks: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }),
|
||||||
|
vmwareSources: this.$store.dispatch(`${ inStore }/findAll`, { type: VMWARE_SOURCE_TYPE }),
|
||||||
|
openstackSources: this.$store.dispatch(`${ inStore }/findAll`, { type: OPENSTACK_SOURCE_TYPE }),
|
||||||
|
ovaSources: this.$store.dispatch(`${ inStore }/findAll`, { type: OVA_SOURCE_TYPE }).catch(() => []),
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await allHash(hash);
|
||||||
|
|
||||||
|
this.allStorageClasses = res.storageClasses;
|
||||||
|
this.allNetworks = res.networks;
|
||||||
|
this.vmwareSources = res.vmwareSources;
|
||||||
|
this.openstackSources = res.openstackSources;
|
||||||
|
this.ovaSources = res.ovaSources;
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
// Ensure the spec object exists to prevent 'undefined' errors during rendering
|
||||||
|
if (!this.value.spec) this.value.spec = {};
|
||||||
|
if (!this.value.spec.sourceCluster) this.value.spec.sourceCluster = {};
|
||||||
|
if (!this.value.spec.networkMapping) this.value.spec.networkMapping = [];
|
||||||
|
|
||||||
|
// Detect if in Edit mode by checking the existing kind
|
||||||
|
// This allows to pre-select the correct Provider Type tab
|
||||||
|
let initialProvider = '';
|
||||||
|
const existingKind = this.value.spec.sourceCluster.kind;
|
||||||
|
|
||||||
|
if (existingKind === VMIMPORT_SOURCE_KINDS.VMWARE) initialProvider = VMIMPORT_SOURCE_PROVIDER.VMWARE;
|
||||||
|
else if (existingKind === VMIMPORT_SOURCE_KINDS.OPENSTACK) initialProvider = VMIMPORT_SOURCE_PROVIDER.OPENSTACK;
|
||||||
|
else if (existingKind === VMIMPORT_SOURCE_KINDS.OVA) initialProvider = VMIMPORT_SOURCE_PROVIDER.OVA;
|
||||||
|
|
||||||
|
// Construct the unique key (Kind/Namespace/Name) if we are editing an existing resource
|
||||||
|
let initialSourceKey = null;
|
||||||
|
|
||||||
|
if (this.value.spec.sourceCluster.name) {
|
||||||
|
initialSourceKey = `${ existingKind }/${ this.value.spec.sourceCluster.namespace }/${ this.value.spec.sourceCluster.name }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allStorageClasses: [],
|
||||||
|
allNetworks: [],
|
||||||
|
vmwareSources: [],
|
||||||
|
openstackSources: [],
|
||||||
|
ovaSources: [],
|
||||||
|
|
||||||
|
// UI State
|
||||||
|
sourceProviderType: initialProvider,
|
||||||
|
selectedSourceKey: initialSourceKey,
|
||||||
|
|
||||||
|
// Static Options
|
||||||
|
providerTypeOptions: [
|
||||||
|
{ label: 'VMware', value: VMIMPORT_SOURCE_PROVIDER.VMWARE },
|
||||||
|
{ label: 'OpenStack', value: VMIMPORT_SOURCE_PROVIDER.OPENSTACK },
|
||||||
|
{ label: 'OVA', value: VMIMPORT_SOURCE_PROVIDER.OVA }
|
||||||
|
],
|
||||||
|
diskBusOptions: [
|
||||||
|
// Allow resetting selection / reset to the default behavior (sending null/empty)
|
||||||
|
{ label: this.t('harvester.addons.vmImport.options.useDefault'), value: '' },
|
||||||
|
{ label: 'VirtIO', value: 'virtio' },
|
||||||
|
{ label: 'SCSI', value: 'scsi' },
|
||||||
|
{ label: 'SATA', value: 'sata' },
|
||||||
|
{ label: 'USB', value: 'usb' },
|
||||||
|
],
|
||||||
|
interfaceModelOptions: [
|
||||||
|
// Allow resetting selection / reset to the default behavior (sending null/empty)
|
||||||
|
{ label: this.t('harvester.addons.vmImport.options.useDefault'), value: '' },
|
||||||
|
{ label: 'VirtIO', value: 'virtio' },
|
||||||
|
{ label: 'e1000', value: 'e1000' },
|
||||||
|
{ label: 'e1000e', value: 'e1000e' },
|
||||||
|
{ label: 'ne2k_pci', value: 'ne2k_pci' },
|
||||||
|
{ label: 'pcnet', value: 'pcnet' },
|
||||||
|
{ label: 'rtl8139', value: 'rtl8139' },
|
||||||
|
],
|
||||||
|
|
||||||
|
fvFormRuleSets: [
|
||||||
|
{ path: 'metadata.name', rules: ['nameRequired'] },
|
||||||
|
{ path: 'spec.virtualMachineName', rules: ['vmNameRequired', 'rfc1123'] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.updateBeforeSave);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
// Return only the sources that match the selected Provider Type (VMware or OpenStack)
|
||||||
|
sourceOptions() {
|
||||||
|
let list = [];
|
||||||
|
|
||||||
|
if (this.sourceProviderType === VMIMPORT_SOURCE_PROVIDER.VMWARE) {
|
||||||
|
list = this.vmwareSources;
|
||||||
|
} else if (this.sourceProviderType === VMIMPORT_SOURCE_PROVIDER.OPENSTACK) {
|
||||||
|
list = this.openstackSources;
|
||||||
|
} else if (this.sourceProviderType === VMIMPORT_SOURCE_PROVIDER.OVA) {
|
||||||
|
list = this.ovaSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.map((s) => {
|
||||||
|
// Fallback for API version/kind if missing on the object
|
||||||
|
let kind = s.kind;
|
||||||
|
|
||||||
|
if (!kind) {
|
||||||
|
if (this.sourceProviderType === VMIMPORT_SOURCE_PROVIDER.VMWARE) kind = VMIMPORT_SOURCE_KINDS.VMWARE;
|
||||||
|
else if (this.sourceProviderType === VMIMPORT_SOURCE_PROVIDER.OPENSTACK) kind = VMIMPORT_SOURCE_KINDS.OPENSTACK;
|
||||||
|
else if (this.sourceProviderType === VMIMPORT_SOURCE_PROVIDER.OVA) kind = VMIMPORT_SOURCE_KINDS.OVA;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiVersion = s.apiVersion || `${ HCI.MIGRATION }/v1beta1`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: s.metadata.name,
|
||||||
|
value: `${ kind }/${ s.metadata.namespace }/${ s.metadata.name }`,
|
||||||
|
// We attach the raw metadata so we can easily populate the spec later without re-finding the object
|
||||||
|
raw: {
|
||||||
|
kind,
|
||||||
|
apiVersion,
|
||||||
|
name: s.metadata.name,
|
||||||
|
namespace: s.metadata.namespace
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
fvExtraRules() {
|
||||||
|
return {
|
||||||
|
nameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.fields.name') }) : undefined,
|
||||||
|
vmNameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.vmName') }) : undefined,
|
||||||
|
rfc1123:
|
||||||
|
(val) => {
|
||||||
|
if (val && !isValidDNSLabelName(val)) {
|
||||||
|
return this.t('harvester.addons.vmImport.errors.rfc1123');
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Perform various form validations before allowing to submit
|
||||||
|
isFormValid() {
|
||||||
|
// Check VM Name is valid
|
||||||
|
const nameError = this.fvNameRule(this.value.spec.virtualMachineName);
|
||||||
|
|
||||||
|
if (nameError) return false;
|
||||||
|
|
||||||
|
// Check mandatory fields in Basics
|
||||||
|
if (!this.value.spec.virtualMachineName) return false;
|
||||||
|
if (!this.selectedSourceKey) return false;
|
||||||
|
|
||||||
|
// Check Network Mappings
|
||||||
|
// If any row is missing source or destination, the form is invalid.
|
||||||
|
const networks = this.value.spec.networkMapping || [];
|
||||||
|
const hasInvalidRow = networks.some((row) => !row.sourceNetwork || !row.destinationNetwork);
|
||||||
|
|
||||||
|
if (hasInvalidRow) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
isNetworkTabInvalid() {
|
||||||
|
const networks = this.value.spec.networkMapping || [];
|
||||||
|
// Only error if a row exists AND it is missing fields
|
||||||
|
|
||||||
|
return networks.some((row) => !row.sourceNetwork || !row.destinationNetwork);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Filter out internal storage classes
|
||||||
|
// to prevent selecting a class that might cause the import to fail
|
||||||
|
storageClassOptions() {
|
||||||
|
return this.allStorageClasses
|
||||||
|
.filter((sc) => {
|
||||||
|
const isInternal = sc.parameters?.['harvesterhci.io/isInternalStorageClass'] === 'true';
|
||||||
|
|
||||||
|
return !isInternal;
|
||||||
|
})
|
||||||
|
.map((sc) => ({
|
||||||
|
label: sc.nameDisplay,
|
||||||
|
value: sc.id
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
networkOptions() {
|
||||||
|
const mgmtOption = {
|
||||||
|
label: 'Management Network',
|
||||||
|
value: MANAGEMENT_NETWORK
|
||||||
|
};
|
||||||
|
|
||||||
|
const vlanOptions = this.allNetworks.map((n) => ({
|
||||||
|
label: n.nameDisplay || n.metadata.name,
|
||||||
|
value: n.id
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [mgmtOption, ...vlanOptions];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
// Clear the selected cluster if the user switches providers (e.g. VMware -> OpenStack)
|
||||||
|
// Prevents submitting a VMware cluster name while the kind is OpenStack
|
||||||
|
onProviderTypeChange(newType) {
|
||||||
|
this.selectedSourceKey = null;
|
||||||
|
this.value.spec.sourceCluster = {};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update the sourceCluster object based on the single dropdown selection
|
||||||
|
updateSource(key) {
|
||||||
|
this.selectedSourceKey = key;
|
||||||
|
const selectedOption = this.sourceOptions.find((o) => o.value === key);
|
||||||
|
|
||||||
|
if (selectedOption) {
|
||||||
|
const {
|
||||||
|
kind, apiVersion, name, namespace
|
||||||
|
} = selectedOption.raw;
|
||||||
|
|
||||||
|
this.value.spec.sourceCluster = {
|
||||||
|
kind,
|
||||||
|
apiVersion,
|
||||||
|
name,
|
||||||
|
namespace
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.value.spec.sourceCluster = {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addNetworkMapping() {
|
||||||
|
this.value.spec.networkMapping.push({
|
||||||
|
sourceNetwork: '',
|
||||||
|
destinationNetwork: '',
|
||||||
|
networkInterfaceModel: ''
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
removeNetworkMapping(index) {
|
||||||
|
if (!this.value?.spec?.networkMapping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index >= 0 && index < this.value.spec.networkMapping.length) {
|
||||||
|
this.value.spec.networkMapping.splice(index, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
requiredRule(val) {
|
||||||
|
if (!val) {
|
||||||
|
return this.t('validation.required', { key: this.t('generic.value') });
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Validates that the input follows Kubernetes Naming Rules (RFC 1123).
|
||||||
|
// If the source VM has uppercase letters or spaces, the user must be warned
|
||||||
|
// that they cannot import it until they rename it on the source. See:
|
||||||
|
// https://docs.harvesterhci.io/v1.6/advanced/addons/vmimport/#source-virtual-machine-name-is-not-rfc1123-compliant
|
||||||
|
fvNameRule(val) {
|
||||||
|
if (!val) return undefined; // 'Required' check handles empty state separately
|
||||||
|
|
||||||
|
// valid RFC 1123
|
||||||
|
if (!isValidDNSLabelName(val)) {
|
||||||
|
return this.t('harvester.addons.vmImport.errors.rfc1123');
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateBeforeSave() {
|
||||||
|
// If networkMapping exists, filter out the "Management Network" rows
|
||||||
|
// Let the vm-import-controller set the default network mapping
|
||||||
|
if (this.value.spec.networkMapping) {
|
||||||
|
this.value.spec.networkMapping = this.value.spec.networkMapping.filter((row) => {
|
||||||
|
return row.destinationNetwork !== MANAGEMENT_NETWORK;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Only handles complex logic that doesn't fit into simple field rules
|
||||||
|
async saveOverride(buttonCb) {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
this.errors = [];
|
||||||
|
|
||||||
|
// Validate Provider Type
|
||||||
|
if (!this.sourceProviderType) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.sourceProvider') }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Network Tab
|
||||||
|
if (this.isNetworkTabInvalid) {
|
||||||
|
errors.push(this.t('harvester.addons.vmImport.errors.networkMappingRequired'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return immediately in case of an error, avoid that `this.save()` runs, preventing `updateBeforeSave` from resetting data.
|
||||||
|
if (errors.length > 0) {
|
||||||
|
this.errors = errors;
|
||||||
|
buttonCb(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only proceed if valid
|
||||||
|
this.save(buttonCb);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:errors="errors"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
:validation-passed="fvFormIsValid"
|
||||||
|
@finish="saveOverride"
|
||||||
|
@error="e=>errors=e"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="{ name: fvGetAndReportPathRules('metadata.name') }"
|
||||||
|
@update:value="$emit('update:value', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tabbed
|
||||||
|
v-bind="$attrs"
|
||||||
|
class="mt-15"
|
||||||
|
:side-tabs="true"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="basic"
|
||||||
|
:label="t('harvester.addons.vmImport.titles.basic')"
|
||||||
|
:weight="3"
|
||||||
|
>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="sourceProviderType"
|
||||||
|
:options="providerTypeOptions"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.sourceProvider')"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="[requiredRule]"
|
||||||
|
required
|
||||||
|
@update:value="onProviderTypeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
:value="selectedSourceKey"
|
||||||
|
:options="sourceOptions"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.sourceCluster')"
|
||||||
|
:placeholder="sourceProviderType ? t('harvester.addons.vmImport.placeholders.selectCluster') : t('harvester.addons.vmImport.placeholders.selectProviderFirst')"
|
||||||
|
:disabled="!sourceProviderType"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="fvGetAndReportPathRules('selectedSourceKey')"
|
||||||
|
required
|
||||||
|
@update:value="updateSource"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="value.spec.virtualMachineName"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.vmName')"
|
||||||
|
:placeholder="t('harvester.addons.vmImport.placeholders.matchSource')"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="fvGetAndReportPathRules('spec.virtualMachineName')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="value.spec.storageClass"
|
||||||
|
:options="storageClassOptions"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.targetStorageClass')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
name="networking"
|
||||||
|
:label="t('harvester.addons.vmImport.titles.networking')"
|
||||||
|
:weight="2"
|
||||||
|
:error="isNetworkTabInvalid"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(row, i) in value.spec.networkMapping"
|
||||||
|
:key="i"
|
||||||
|
class="network-row box mb-10"
|
||||||
|
>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="row.sourceNetwork"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.sourceNetwork')"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="[requiredRule]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="row.destinationNetwork"
|
||||||
|
:options="networkOptions"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.destNetwork')"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="[requiredRule]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="row.networkInterfaceModel"
|
||||||
|
:options="interfaceModelOptions"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.interfaceModel')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-1 remove-btn-container">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn role-link"
|
||||||
|
@click="removeNetworkMapping(i)"
|
||||||
|
>
|
||||||
|
{{ t('harvester.addons.vmImport.actions.remove') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn role-secondary"
|
||||||
|
@click="addNetworkMapping"
|
||||||
|
>
|
||||||
|
{{ t('harvester.addons.vmImport.actions.addNetwork') }}
|
||||||
|
</button>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
name="advanced"
|
||||||
|
:label="t('harvester.addons.vmImport.titles.advanced')"
|
||||||
|
:weight="1"
|
||||||
|
>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="value.spec.folder"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.folder')"
|
||||||
|
:placeholder="t('harvester.addons.vmImport.placeholders.folderExample')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="value.spec.defaultDiskBusType"
|
||||||
|
:options="diskBusOptions"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.diskBus')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="value.spec.defaultNetworkInterfaceModel"
|
||||||
|
:options="interfaceModelOptions"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.defaultInterface')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<Checkbox
|
||||||
|
v-model:value="value.spec.skipPreflightChecks"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.skipPreflight')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
v-model:value="value.spec.forcePowerOff"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.forcePowerOff')"
|
||||||
|
:mode="mode"
|
||||||
|
class="mt-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.network-row {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background: var(--body-bg);
|
||||||
|
}
|
||||||
|
.remove-btn-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
302
pkg/harvester/edit/migration.harvesterhci.io.vmwaresource.vue
Normal file
302
pkg/harvester/edit/migration.harvesterhci.io.vmwaresource.vue
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
<script>
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import FormValidation from '@shell/mixins/form-validation';
|
||||||
|
import { SECRET } from '@shell/config/types';
|
||||||
|
import { randomStr } from '@shell/utils/string';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditVmwareSource',
|
||||||
|
|
||||||
|
// Declare the event, fixes a console warning
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
CruResource,
|
||||||
|
Tabbed,
|
||||||
|
Tab,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
NameNsDescription,
|
||||||
|
RadioGroup,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView, FormValidation],
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
this.allSecrets = await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
if (!this.value.spec) this.value.spec = {};
|
||||||
|
if (!this.value.spec.credentials) this.value.spec.credentials = {};
|
||||||
|
|
||||||
|
const initialMode = this.value.spec.credentials.name ? 'existing' : 'new';
|
||||||
|
|
||||||
|
return {
|
||||||
|
allSecrets: [],
|
||||||
|
authMode: initialMode,
|
||||||
|
newUsername: '',
|
||||||
|
newPassword: '',
|
||||||
|
newCaCert: '',
|
||||||
|
|
||||||
|
fvFormRuleSets: [
|
||||||
|
{ path: 'metadata.name', rules: ['nameRequired'] },
|
||||||
|
{ path: 'spec.endpoint', rules: ['endpointRequired'] },
|
||||||
|
{ path: 'spec.dc', rules: ['dcRequired'] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
authModeOptions() {
|
||||||
|
return [
|
||||||
|
{ label: this.t('harvester.addons.vmImport.fields.createSecret'), value: 'new' },
|
||||||
|
{ label: this.t('harvester.addons.vmImport.fields.useSecret'), value: 'existing' }
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
secretOptions() {
|
||||||
|
const currentNamespace = this.value.metadata.namespace || 'default';
|
||||||
|
|
||||||
|
return this.allSecrets
|
||||||
|
.filter((s) => s.metadata.namespace === currentNamespace)
|
||||||
|
.map((s) => ({
|
||||||
|
label: s.nameDisplay,
|
||||||
|
value: s.metadata.name
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
// Define custom rules for the FormValidation mixin
|
||||||
|
fvExtraRules() {
|
||||||
|
return {
|
||||||
|
nameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.fields.name') }) : undefined,
|
||||||
|
endpointRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.vmware.fields.endpoint') }) : undefined,
|
||||||
|
dcRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.vmware.fields.datacenter') }) : undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
isFormValid() {
|
||||||
|
if (!this.fvFormIsValid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.authMode === 'new') {
|
||||||
|
if (!this.newUsername || !this.newPassword) return false;
|
||||||
|
} else {
|
||||||
|
if (!this.value.spec.credentials.name) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
usernameRule(val) {
|
||||||
|
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.username') }) : undefined;
|
||||||
|
},
|
||||||
|
passwordRule(val) {
|
||||||
|
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.password') }) : undefined;
|
||||||
|
},
|
||||||
|
secretRule(val) {
|
||||||
|
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.selectSecret') }) : undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveSource(buttonCb) {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.authMode === 'new') {
|
||||||
|
const secretName = `${ this.value.metadata.name }-creds-${ randomStr(4).toLowerCase() }`;
|
||||||
|
const namespace = this.value.metadata.namespace || 'default';
|
||||||
|
|
||||||
|
// Create the model with the correct Schema ID (SECRET)
|
||||||
|
const newSecret = await this.$store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: SECRET,
|
||||||
|
metadata: {
|
||||||
|
name: secretName,
|
||||||
|
namespace
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use '_type' to set the Kubernetes 'type' field.
|
||||||
|
newSecret['_type'] = 'Opaque';
|
||||||
|
|
||||||
|
// base64 encode the data
|
||||||
|
newSecret['data'] = {
|
||||||
|
username: btoa(this.newUsername),
|
||||||
|
password: btoa(this.newPassword),
|
||||||
|
// Only include CA cert if the user provided one
|
||||||
|
caCert: this.newCaCert ? btoa(this.newCaCert) : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
await newSecret.save();
|
||||||
|
|
||||||
|
// Link the new secret to the Source
|
||||||
|
this.value.spec.credentials = {
|
||||||
|
name: secretName,
|
||||||
|
namespace
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.save(buttonCb);
|
||||||
|
} catch (err) {
|
||||||
|
this.errors = [err];
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:errors="errors"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
:validation-passed="isFormValid"
|
||||||
|
@finish="saveSource"
|
||||||
|
@error="e=>errors=e"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="{ name: fvGetAndReportPathRules('metadata.name') }"
|
||||||
|
@update:value="$emit('update:value', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tabbed
|
||||||
|
v-bind="$attrs"
|
||||||
|
class="mt-15"
|
||||||
|
:side-tabs="true"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="basic"
|
||||||
|
:label="t('harvester.addons.vmImport.titles.basic')"
|
||||||
|
:weight="2"
|
||||||
|
>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="value.spec.endpoint"
|
||||||
|
:label="t('harvester.addons.vmImport.vmware.fields.endpoint')"
|
||||||
|
:placeholder="t('harvester.addons.vmImport.vmware.placeholders.endpoint')"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="fvGetAndReportPathRules('spec.endpoint')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="value.spec.dc"
|
||||||
|
:label="t('harvester.addons.vmImport.vmware.fields.datacenter')"
|
||||||
|
:placeholder="t('harvester.addons.vmImport.vmware.placeholders.datacenter')"
|
||||||
|
:tooltip="t('harvester.addons.vmImport.vmware.tooltips.datacenter')"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="fvGetAndReportPathRules('spec.dc')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
name="auth"
|
||||||
|
:label="t('harvester.addons.vmImport.titles.auth')"
|
||||||
|
:weight="1"
|
||||||
|
>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="authMode"
|
||||||
|
name="authMode"
|
||||||
|
:options="authModeOptions"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="authMode === 'new'">
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="newUsername"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.username')"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="[usernameRule]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="newPassword"
|
||||||
|
type="password"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.password')"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="[passwordRule]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="newCaCert"
|
||||||
|
type="multiline"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.caCert')"
|
||||||
|
:placeholder="t('harvester.addons.vmImport.placeholders.caCert')"
|
||||||
|
:min-height="100"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-muted">
|
||||||
|
Note: A new Kubernetes Secret will be created to store these credentials.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="authMode === 'existing'">
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="value.spec.credentials.name"
|
||||||
|
:options="secretOptions"
|
||||||
|
:label="t('harvester.addons.vmImport.fields.selectSecret')"
|
||||||
|
:mode="mode"
|
||||||
|
:rules="[secretRule]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
@ -20,6 +20,7 @@ nav:
|
|||||||
Monitoring: Monitoring
|
Monitoring: Monitoring
|
||||||
Logging: Logging
|
Logging: Logging
|
||||||
'Monitoring and Logging': Monitoring and Logging
|
'Monitoring and Logging': Monitoring and Logging
|
||||||
|
vmimport: Virtual Machine Imports
|
||||||
|
|
||||||
resourceTable:
|
resourceTable:
|
||||||
groupBy:
|
groupBy:
|
||||||
@ -154,15 +155,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
|
||||||
@ -185,12 +190,16 @@ harvester:
|
|||||||
title: Restart Virtual Machine
|
title: Restart Virtual Machine
|
||||||
tip: Restart the virtual machine for configuration changes to take effect.
|
tip: Restart the virtual machine for configuration changes to take effect.
|
||||||
cancel: Save
|
cancel: Save
|
||||||
|
|
||||||
notification:
|
notification:
|
||||||
title:
|
title:
|
||||||
succeed: Succeed
|
succeed: Succeed
|
||||||
info: Info
|
info: Info
|
||||||
warning: Warning
|
warning: Warning
|
||||||
error: Error
|
error: Error
|
||||||
|
restartRequired:
|
||||||
|
title: '{count} {count, plural, =1 {Virtual Machine is} other {Virtual Machines are}} Pending Restart'
|
||||||
|
message: 'Please restart { vmNames } to apply updated configurations'
|
||||||
action:
|
action:
|
||||||
createVM: Create Virtual Machine
|
createVM: Create Virtual Machine
|
||||||
start: Start
|
start: Start
|
||||||
@ -226,7 +235,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
|
||||||
@ -290,6 +300,17 @@ harvester:
|
|||||||
totalSnapshotQuota: Total Snapshot Quota
|
totalSnapshotQuota: Total Snapshot Quota
|
||||||
storageClass: Storage Class
|
storageClass: Storage Class
|
||||||
restore: Restore
|
restore: Restore
|
||||||
|
vmImportSourceVm: Source VM
|
||||||
|
vmImportSourceCluster: Source Cluster
|
||||||
|
vmImportStatus: Import Status
|
||||||
|
vmImportSourceVDatacenter: Datacenter
|
||||||
|
vmImportSourceVEndpoint: Endpoint
|
||||||
|
vmImportSourceVClusterStatus: Cluster Status
|
||||||
|
vmImportSourceORegion: Region
|
||||||
|
vmImportSourceOEndpoint: Endpoint
|
||||||
|
vmImportSourceOClusterStatus: Cluster Status
|
||||||
|
vmImportSourceOVAUrl: URL
|
||||||
|
vmImportSourceOVAStatus: Status
|
||||||
tab:
|
tab:
|
||||||
volume: Volumes
|
volume: Volumes
|
||||||
network: Networks
|
network: Networks
|
||||||
@ -352,6 +373,9 @@ harvester:
|
|||||||
claimError: Error enabling passthrough on {name}
|
claimError: Error enabling passthrough on {name}
|
||||||
unclaimError: Error disabling passthrough on {name}
|
unclaimError: Error disabling passthrough on {name}
|
||||||
cantUnclaim: You cannot disable passthrough on a device claimed by another user.
|
cantUnclaim: You cannot disable passthrough on a device claimed by another user.
|
||||||
|
detachWarning:
|
||||||
|
title: Cannot Disable Passthrough
|
||||||
|
message: Please detach the device from the VM and save it first before disabling passthrough.
|
||||||
enableGroup: Enable Group
|
enableGroup: Enable Group
|
||||||
disableGroup: Disable Group
|
disableGroup: Disable Group
|
||||||
labelRequired: "This rule should not be manually altered: it ensures that the PCI devices selected for this virtual machine are available on the virtual machine's host."
|
labelRequired: "This rule should not be manually altered: it ensures that the PCI devices selected for this virtual machine are available on the virtual machine's host."
|
||||||
@ -442,6 +466,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 +539,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 +621,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 +662,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 +941,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
|
||||||
@ -958,7 +1004,9 @@ harvester:
|
|||||||
createTitle: Create Schedule
|
createTitle: Create Schedule
|
||||||
createButtonText: Create Schedule
|
createButtonText: Create Schedule
|
||||||
scheduleType: Virtual Machine Schedule Type
|
scheduleType: Virtual Machine Schedule Type
|
||||||
cron: Cron Schedule
|
cron:
|
||||||
|
label: Cron Schedule
|
||||||
|
editButton: Edit
|
||||||
detail:
|
detail:
|
||||||
namespace: Namespace
|
namespace: Namespace
|
||||||
sourceVM: Source Virtual Machine
|
sourceVM: Source Virtual Machine
|
||||||
@ -1064,6 +1112,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 +1229,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.'
|
||||||
@ -1249,7 +1299,10 @@ harvester:
|
|||||||
deleteImage: Please select an image to delete.
|
deleteImage: Please select an image to delete.
|
||||||
deleteSuccess: "{name} deleted successfully."
|
deleteSuccess: "{name} deleted successfully."
|
||||||
imagePreloadStrategy: Image Preload Strategy
|
imagePreloadStrategy: Image Preload Strategy
|
||||||
|
nodeUpgradeOption: Node Upgrade Option
|
||||||
restoreVM: Restore VM
|
restoreVM: Restore VM
|
||||||
|
strategy: Strategy
|
||||||
|
pauseNodes: Pause Nodes
|
||||||
strategyType: Strategy Type
|
strategyType: Strategy Type
|
||||||
concurrency: Concurrency
|
concurrency: Concurrency
|
||||||
harvesterMonitoring:
|
harvesterMonitoring:
|
||||||
@ -1534,10 +1587,85 @@ 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
|
||||||
|
auth: Authentication
|
||||||
pvc: Volume
|
pvc: Volume
|
||||||
|
networking: Network Mapping
|
||||||
|
advanced: Advanced
|
||||||
|
labels:
|
||||||
|
vmimport: Virtual Machine Import
|
||||||
|
vmimportSourceVMWare: Source VMWare
|
||||||
|
vmimportSourceOpenStack: Source OpenStack
|
||||||
|
vmimportSourceOVA: Source OVA
|
||||||
|
fields:
|
||||||
|
sourceProvider: Source Provider Type
|
||||||
|
sourceCluster: Source Cluster
|
||||||
|
vmName: VM Name
|
||||||
|
targetStorageClass: Target Storage Class
|
||||||
|
sourceNetwork: Source Network Name
|
||||||
|
destNetwork: Destination Network
|
||||||
|
interfaceModel: Interface Model
|
||||||
|
folder: Folder
|
||||||
|
diskBus: Default Disk Bus
|
||||||
|
defaultInterface: Default Network Interface
|
||||||
|
skipPreflight: Skip Preflight Checks
|
||||||
|
forcePowerOff: Force Power Off Source VM
|
||||||
|
username: Username
|
||||||
|
password: Password
|
||||||
|
caCert: CA Certificate (PEM)
|
||||||
|
selectSecret: Select Secret
|
||||||
|
createSecret: Create New Credentials
|
||||||
|
useSecret: Use Existing Secret
|
||||||
|
none: None (Public URL)
|
||||||
|
placeholders:
|
||||||
|
selectCluster: Select a cluster...
|
||||||
|
selectProviderFirst: Select a provider type first
|
||||||
|
matchSource: Must match the name in the source cluster
|
||||||
|
folderExample: e.g. /Datacenters/DC1/vm
|
||||||
|
caCert: "-----BEGIN CERTIFICATE----- ..."
|
||||||
|
options:
|
||||||
|
useDefault: Use Default
|
||||||
|
actions:
|
||||||
|
addNetwork: Add Network Mapping
|
||||||
|
remove: Remove
|
||||||
|
errors:
|
||||||
|
rfc1123: 'Invalid format. Name must be lowercase, alphanumeric, and cannot contain spaces (e.g. "my-vm-1"). If your Source VM name does not match this, you must rename it on the Source cluster first.'
|
||||||
|
networkMappingRequired: Every Network Mapping row must have a Source and Destination selected.
|
||||||
|
openstack:
|
||||||
|
fields:
|
||||||
|
endpoint: Identity Service Endpoint
|
||||||
|
region: Region
|
||||||
|
projectName: Project Name
|
||||||
|
domainName: Domain Name
|
||||||
|
retryCount: Upload Image Retry Count
|
||||||
|
retryDelay: Upload Image Retry Delay
|
||||||
|
placeholders:
|
||||||
|
endpoint: "e.g. https://devstack/identity"
|
||||||
|
region: e.g. RegionOne
|
||||||
|
projectName: e.g. admin
|
||||||
|
domainName: e.g. default
|
||||||
|
retryCount: "Default: 30"
|
||||||
|
retryDelay: "Default: 10"
|
||||||
|
vmware:
|
||||||
|
fields:
|
||||||
|
endpoint: vCenter Endpoint
|
||||||
|
datacenter: Datacenter
|
||||||
|
placeholders:
|
||||||
|
endpoint: "e.g. https://vscim/sdk"
|
||||||
|
datacenter: e.g. DC0
|
||||||
|
tooltips:
|
||||||
|
datacenter: The exact name of the Datacenter object in vCenter
|
||||||
|
ova:
|
||||||
|
fields:
|
||||||
|
url: OVA URL
|
||||||
|
httpTimeout: HTTP Timeout
|
||||||
|
placeholders:
|
||||||
|
url: "e.g. https://download.example.com/images/my-vm.ova"
|
||||||
|
|
||||||
rancherVcluster:
|
rancherVcluster:
|
||||||
accessRancher: Access the Rancher Dashboard
|
accessRancher: Access the Rancher Dashboard
|
||||||
hostname: Hostname
|
hostname: Hostname
|
||||||
@ -1703,6 +1831,9 @@ harvester:
|
|||||||
vgpu:
|
vgpu:
|
||||||
label: vGPU Devices
|
label: vGPU Devices
|
||||||
noPermission: Please contact system administrator to add Harvester add-ons first.
|
noPermission: Please contact system administrator to add Harvester add-ons first.
|
||||||
|
detachWarning:
|
||||||
|
title: Cannot Disable vGPU
|
||||||
|
message: Please detach the device from the VM and save it first before disabling this vGPU device.
|
||||||
goSetting:
|
goSetting:
|
||||||
prefix: The nvidia-driver-toolkit add-on is not enabled, click
|
prefix: The nvidia-driver-toolkit add-on is not enabled, click
|
||||||
middle: here
|
middle: here
|
||||||
@ -1737,6 +1868,9 @@ harvester:
|
|||||||
claimError: Error enabling passthrough on {name}
|
claimError: Error enabling passthrough on {name}
|
||||||
unclaimError: Error disabling passthrough on {name}
|
unclaimError: Error disabling passthrough on {name}
|
||||||
cantUnclaim: You cannot disable passthrough on a device claimed by another user.
|
cantUnclaim: You cannot disable passthrough on a device claimed by another user.
|
||||||
|
detachWarning:
|
||||||
|
title: Cannot Disable Passthrough
|
||||||
|
message: Please detach the device from the VM and save it first before disabling passthrough.
|
||||||
enablePassthroughWarning: 'Please re-enable the USB device if the device path changes in the following situations:<br/> 1) Re-plugging the USB device.<br/> 2) Rebooting the node.<br/><br/>An incorrect device path may cause passthrough to fail.'
|
enablePassthroughWarning: 'Please re-enable the USB device if the device path changes in the following situations:<br/> 1) Re-plugging the USB device.<br/> 2) Rebooting the node.<br/><br/>An incorrect device path may cause passthrough to fail.'
|
||||||
|
|
||||||
harvesterVlanConfigMigrateDialog:
|
harvesterVlanConfigMigrateDialog:
|
||||||
|
|||||||
@ -64,6 +64,10 @@ export default {
|
|||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
const rows = this.$store.getters[`${ inStore }/all`](HCI.PCI_DEVICE);
|
const rows = this.$store.getters[`${ inStore }/all`](HCI.PCI_DEVICE);
|
||||||
|
|
||||||
|
rows.forEach((row) => {
|
||||||
|
row.allowDisable = true;
|
||||||
|
});
|
||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -54,7 +54,13 @@ export default {
|
|||||||
devices() {
|
devices() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
return this.$store.getters[`${ inStore }/all`](HCI.USB_DEVICE) || [];
|
const data = this.$store.getters[`${ inStore }/all`](HCI.USB_DEVICE) || [];
|
||||||
|
|
||||||
|
data.forEach((row) => {
|
||||||
|
row.allowDisable = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -65,6 +65,10 @@ export default {
|
|||||||
const vGpuDevices = this.$store.getters[`${ inStore }/all`](HCI.VGPU_DEVICE) || [];
|
const vGpuDevices = this.$store.getters[`${ inStore }/all`](HCI.VGPU_DEVICE) || [];
|
||||||
const srioVGpuDevices = this.$store.getters[`${ inStore }/all`](HCI.SR_IOVGPU_DEVICE) || [];
|
const srioVGpuDevices = this.$store.getters[`${ inStore }/all`](HCI.SR_IOVGPU_DEVICE) || [];
|
||||||
|
|
||||||
|
vGpuDevices.forEach((row) => {
|
||||||
|
row.allowDisable = true;
|
||||||
|
});
|
||||||
|
|
||||||
if (this.hasSRIOVGPUSchema) {
|
if (this.hasSRIOVGPUSchema) {
|
||||||
return vGpuDevices.filter((device) => !!srioVGpuDevices.find((s) => s.isEnabled && s.spec?.nodeName === device.spec?.nodeName));
|
return vGpuDevices.filter((device) => !!srioVGpuDevices.find((s) => s.isEnabled && s.spec?.nodeName === device.spec?.nodeName));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,60 @@
|
|||||||
|
<script>
|
||||||
|
import ResourceTable from '@shell/components/ResourceTable';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import { SCHEMA } from '@shell/config/types';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
id: HCI.VMIMPORT_SOURCE_O,
|
||||||
|
type: SCHEMA,
|
||||||
|
attributes: {
|
||||||
|
kind: HCI.VMIMPORT_SOURCE_O,
|
||||||
|
namespaced: true
|
||||||
|
},
|
||||||
|
metadata: { name: HCI.VMIMPORT_SOURCE_O },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterVMImportSourceO',
|
||||||
|
components: { ResourceTable, Loading },
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIMPORT_SOURCE_O });
|
||||||
|
|
||||||
|
const configSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.VMIMPORT_SOURCE_O);
|
||||||
|
|
||||||
|
if (!configSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) {
|
||||||
|
this.$store.dispatch('type-map/configureType', { match: HCI.VMIMPORT_SOURCE_O, isCreatable: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { rows: [] };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
schema() {
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
typeDisplay() {
|
||||||
|
return this.$store.getters['type-map/labelFor'](schema, 99);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<ResourceTable
|
||||||
|
v-else
|
||||||
|
v-bind="$attrs"
|
||||||
|
:groupable="true"
|
||||||
|
:schema="schema"
|
||||||
|
:rows="rows"
|
||||||
|
key-field="_key"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
<script>
|
||||||
|
import ResourceTable from '@shell/components/ResourceTable';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import { SCHEMA } from '@shell/config/types';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
id: HCI.VMIMPORT,
|
||||||
|
type: SCHEMA,
|
||||||
|
attributes: {
|
||||||
|
kind: HCI.VMIMPORT,
|
||||||
|
namespaced: true
|
||||||
|
},
|
||||||
|
metadata: { name: HCI.VMIMPORT },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterVMImportVirtualMachine',
|
||||||
|
components: { ResourceTable, Loading },
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIMPORT });
|
||||||
|
|
||||||
|
const configSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.VMIMPORT);
|
||||||
|
|
||||||
|
if (!configSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) {
|
||||||
|
this.$store.dispatch('type-map/configureType', { match: HCI.VMIMPORT, isCreatable: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { rows: [] };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
schema() {
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
typeDisplay() {
|
||||||
|
return this.$store.getters['type-map/labelFor'](schema, 99);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<ResourceTable
|
||||||
|
v-else
|
||||||
|
v-bind="$attrs"
|
||||||
|
:groupable="true"
|
||||||
|
:schema="schema"
|
||||||
|
:rows="rows"
|
||||||
|
key-field="_key"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
<script>
|
||||||
|
import ResourceTable from '@shell/components/ResourceTable';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import { SCHEMA } from '@shell/config/types';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
id: HCI.VMIMPORT_SOURCE_V,
|
||||||
|
type: SCHEMA,
|
||||||
|
attributes: {
|
||||||
|
kind: HCI.VMIMPORT_SOURCE_V,
|
||||||
|
namespaced: true
|
||||||
|
},
|
||||||
|
metadata: { name: HCI.VMIMPORT_SOURCE_V },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterVMImportSourceV',
|
||||||
|
components: { ResourceTable, Loading },
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIMPORT_SOURCE_V });
|
||||||
|
|
||||||
|
const configSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.VMIMPORT_SOURCE_V);
|
||||||
|
|
||||||
|
if (!configSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) {
|
||||||
|
this.$store.dispatch('type-map/configureType', { match: HCI.VMIMPORT_SOURCE_V, isCreatable: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { rows: [] };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
schema() {
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
typeDisplay() {
|
||||||
|
return this.$store.getters['type-map/labelFor'](schema, 99);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<ResourceTable
|
||||||
|
v-else
|
||||||
|
v-bind="$attrs"
|
||||||
|
:groupable="true"
|
||||||
|
:schema="schema"
|
||||||
|
:rows="rows"
|
||||||
|
key-field="_key"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@ -110,11 +110,12 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
hasNode: false,
|
hasNode: false,
|
||||||
allVMs: [],
|
allVMs: [],
|
||||||
allVMIs: [],
|
allVMIs: [],
|
||||||
allNodeNetworks: [],
|
allNodeNetworks: [],
|
||||||
allClusterNetworks: [],
|
allClusterNetworks: [],
|
||||||
|
restartNotificationDisplayed: false,
|
||||||
HCI
|
HCI
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -174,6 +175,48 @@ export default {
|
|||||||
this['allVMIs'] = vmis;
|
this['allVMIs'] = vmis;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
beforeUnmount() {
|
||||||
|
// clear restart message before component unmount
|
||||||
|
this.$store.dispatch('growl/clear');
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
allVMs: {
|
||||||
|
handler(neu) {
|
||||||
|
const vmNames = [];
|
||||||
|
|
||||||
|
neu.forEach((vm) => {
|
||||||
|
if (vm.isRestartRequired) {
|
||||||
|
vmNames.push(vm.metadata.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const count = vmNames.length;
|
||||||
|
|
||||||
|
if ( count === 0 && this.restartNotificationDisplayed) {
|
||||||
|
this.restartNotificationDisplayed = false;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
// clear old notification before showing new one
|
||||||
|
if (this.restartNotificationDisplayed) {
|
||||||
|
this.$store.dispatch('growl/clear');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count > 0 && vmNames.length > 0) {
|
||||||
|
this.$store.dispatch('growl/warning', {
|
||||||
|
title: this.t('harvester.notification.restartRequired.title', { count }),
|
||||||
|
message: this.t('harvester.notification.restartRequired.message', { vmNames: vmNames.join(', ') }),
|
||||||
|
timeout: 10000,
|
||||||
|
}, { root: true });
|
||||||
|
this.restartNotificationDisplayed = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
lockIconTooltipMessage(row) {
|
lockIconTooltipMessage(row) {
|
||||||
const message = '';
|
const message = '';
|
||||||
@ -243,6 +286,12 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.growl-container {
|
||||||
|
z-index: 56 !important; // set to be lower than the vm action menu (z-index: 57)
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.state {
|
.state {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -144,6 +144,12 @@ export default class PCIDevice extends SteveModel {
|
|||||||
// 'disable' passthrough deletes claim
|
// 'disable' passthrough deletes claim
|
||||||
// backend should return error if device is in use
|
// backend should return error if device is in use
|
||||||
async disablePassthrough() {
|
async disablePassthrough() {
|
||||||
|
if (!this.allowDisable) {
|
||||||
|
this.showDetachWarning();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!this.claimedByMe) {
|
if (!this.claimedByMe) {
|
||||||
throw new Error(this.$rootGetters['i18n/t']('harvester.pci.cantUnclaim', { name: escapeHtml(this.metadata.name) }));
|
throw new Error(this.$rootGetters['i18n/t']('harvester.pci.cantUnclaim', { name: escapeHtml(this.metadata.name) }));
|
||||||
@ -169,4 +175,20 @@ export default class PCIDevice extends SteveModel {
|
|||||||
get groupByDevice() {
|
get groupByDevice() {
|
||||||
return this.status?.description;
|
return this.status?.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showDetachWarning() {
|
||||||
|
this.$dispatch('growl/warning', {
|
||||||
|
title: this.$rootGetters['i18n/t']('harvester.pci.detachWarning.title'),
|
||||||
|
message: this.$rootGetters['i18n/t']('harvester.pci.detachWarning.message'),
|
||||||
|
timeout: 5000
|
||||||
|
}, { root: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
get allowDisable() {
|
||||||
|
return this._allowDisable;
|
||||||
|
}
|
||||||
|
|
||||||
|
set allowDisable(value) {
|
||||||
|
this._allowDisable = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -133,6 +133,12 @@ export default class USBDevice extends SteveModel {
|
|||||||
// 'disable' passthrough deletes claim
|
// 'disable' passthrough deletes claim
|
||||||
// backend should return error if device is in use
|
// backend should return error if device is in use
|
||||||
async disablePassthrough() {
|
async disablePassthrough() {
|
||||||
|
if (!this.allowDisable) {
|
||||||
|
this.showDetachWarning();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!this.claimedByMe) {
|
if (!this.claimedByMe) {
|
||||||
throw new Error(this.$rootGetters['i18n/t']('harvester.usb.cantUnclaim', { name: escapeHtml(this.metadata.name) }));
|
throw new Error(this.$rootGetters['i18n/t']('harvester.usb.cantUnclaim', { name: escapeHtml(this.metadata.name) }));
|
||||||
@ -158,4 +164,20 @@ export default class USBDevice extends SteveModel {
|
|||||||
get groupByDevice() {
|
get groupByDevice() {
|
||||||
return this.status?.description;
|
return this.status?.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showDetachWarning() {
|
||||||
|
this.$dispatch('growl/warning', {
|
||||||
|
title: this.$rootGetters['i18n/t']('harvester.usb.detachWarning.title'),
|
||||||
|
message: this.$rootGetters['i18n/t']('harvester.usb.detachWarning.message'),
|
||||||
|
timeout: 5000
|
||||||
|
}, { root: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
get allowDisable() {
|
||||||
|
return this._allowDisable;
|
||||||
|
}
|
||||||
|
|
||||||
|
set allowDisable(value) {
|
||||||
|
this._allowDisable = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -100,6 +100,12 @@ export default class VGpuDevice extends SteveModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async disableVGpu() {
|
async disableVGpu() {
|
||||||
|
if (!this.allowDisable) {
|
||||||
|
this.showDetachWarning();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { vGPUTypeName, enabled } = this.spec;
|
const { vGPUTypeName, enabled } = this.spec;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -126,4 +132,20 @@ export default class VGpuDevice extends SteveModel {
|
|||||||
get vGpuAvailableTypes() {
|
get vGpuAvailableTypes() {
|
||||||
return this.status?.availableTypes ? Object.keys(this.status.availableTypes) : [];
|
return this.status?.availableTypes ? Object.keys(this.status.availableTypes) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showDetachWarning() {
|
||||||
|
this.$dispatch('growl/warning', {
|
||||||
|
title: this.$rootGetters['i18n/t']('harvester.vgpu.detachWarning.title'),
|
||||||
|
message: this.$rootGetters['i18n/t']('harvester.vgpu.detachWarning.message'),
|
||||||
|
timeout: 5000
|
||||||
|
}, { root: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
get allowDisable() {
|
||||||
|
return this._allowDisable;
|
||||||
|
}
|
||||||
|
|
||||||
|
set allowDisable(value) {
|
||||||
|
this._allowDisable = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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 = [
|
||||||
|
|||||||
@ -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 });
|
||||||
|
|||||||
@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.actions?.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'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1161,11 +1180,15 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get stateDescription() {
|
get isRestartRequired() {
|
||||||
const conditions = get(this, 'status.conditions');
|
const conditions = get(this, 'status.conditions');
|
||||||
const restartRequired = findBy(conditions, 'type', 'RestartRequired');
|
const restartRequired = findBy(conditions, 'type', 'RestartRequired');
|
||||||
|
|
||||||
if (restartRequired && restartRequired.status === 'True') {
|
return restartRequired && restartRequired.status === 'True';
|
||||||
|
}
|
||||||
|
|
||||||
|
get stateDescription() {
|
||||||
|
if (this.isRestartRequired) {
|
||||||
return this.t('harvester.virtualMachine.hotplug.restartVMMessage');
|
return this.t('harvester.virtualMachine.hotplug.restartVMMessage');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1240,6 +1263,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');
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester",
|
"name": "harvester",
|
||||||
"description": "Rancher UI Extension for Harvester",
|
"description": "Rancher UI Extension for Harvester",
|
||||||
"version": "1.7.0-dev",
|
"version": "1.8.0-dev",
|
||||||
"private": false,
|
"private": false,
|
||||||
"rancher": {
|
"rancher": {
|
||||||
"annotations": {
|
"annotations": {
|
||||||
"catalog.cattle.io/display-name": "Harvester",
|
"catalog.cattle.io/display-name": "Harvester",
|
||||||
"catalog.cattle.io/kube-version": ">= 1.16.0-0",
|
"catalog.cattle.io/kube-version": ">= 1.16.0-0",
|
||||||
"catalog.cattle.io/rancher-version": ">= 2.13.0-0",
|
"catalog.cattle.io/rancher-version": ">= 2.14.0-0",
|
||||||
"catalog.cattle.io/ui-extensions-version": ">= 3.0.0 < 4.0.0"
|
"catalog.cattle.io/ui-extensions-version": ">= 3.0.0 < 4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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',
|
||||||
@ -55,6 +56,11 @@ export const HCI = {
|
|||||||
IP_POOL: 'loadbalancer.harvesterhci.io.ippool',
|
IP_POOL: 'loadbalancer.harvesterhci.io.ippool',
|
||||||
HARVESTER_CONFIG: 'rke-machine-config.cattle.io.harvesterconfig',
|
HARVESTER_CONFIG: 'rke-machine-config.cattle.io.harvesterconfig',
|
||||||
LVM_VOLUME_GROUP: 'harvesterhci.io.lvmvolumegroup',
|
LVM_VOLUME_GROUP: 'harvesterhci.io.lvmvolumegroup',
|
||||||
|
VMIMPORT_SOURCE_V: 'migration.harvesterhci.io.vmwaresource',
|
||||||
|
VMIMPORT_SOURCE_O: 'migration.harvesterhci.io.openstacksource',
|
||||||
|
VMIMPORT_SOURCE_OVA: 'migration.harvesterhci.io.ovasource',
|
||||||
|
VMIMPORT: 'migration.harvesterhci.io.virtualmachineimport',
|
||||||
|
MIGRATION: 'migration.harvesterhci.io',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot';
|
export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot';
|
||||||
|
|||||||
99
pkg/harvester/utils/dynamic-nav.js
Normal file
99
pkg/harvester/utils/dynamic-nav.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* Dynamically toggles SideNav entries based on the enabled status of a specific Addon.
|
||||||
|
*
|
||||||
|
* @param {Object} store - The Vuex store instance.
|
||||||
|
* @param {String} productName - The product name (e.g. 'harvester').
|
||||||
|
* @param {Object} config - Configuration object.
|
||||||
|
* @param {String} config.addonName - The name of the addon to watch.
|
||||||
|
* @param {String} config.resourceType - The schema ID for addons.
|
||||||
|
* @param {String} config.navGroup - The group name in the side nav.
|
||||||
|
* @param {Array<String>} config.types - Array of Resource IDs to show/hide.
|
||||||
|
*/
|
||||||
|
export function registerAddonSideNav(store, productName, {
|
||||||
|
addonName, resourceType, navGroup, types
|
||||||
|
}) {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forces the SideNav component to re-render by toggling a dummy user preference.
|
||||||
|
// Necessary because the menu component does not automatically detect
|
||||||
|
// changes to the allowed types list.
|
||||||
|
const kickSideNav = () => {
|
||||||
|
const TRIGGER = 'ui.refresh.trigger';
|
||||||
|
|
||||||
|
store.dispatch('type-map/addFavorite', TRIGGER);
|
||||||
|
|
||||||
|
// SideNav component seem to ignore rapid state changes.
|
||||||
|
// Wait 600ms to ensure the toggle event triggers a re-render.
|
||||||
|
setTimeout(() => {
|
||||||
|
store.dispatch('type-map/removeFavorite', TRIGGER);
|
||||||
|
}, 600);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Adds or removes the resource IDs from the product visibility whitelist.
|
||||||
|
const setMenuVisibility = (visible) => {
|
||||||
|
if (visible) {
|
||||||
|
store.commit('type-map/basicType', {
|
||||||
|
product: productName,
|
||||||
|
group: navGroup,
|
||||||
|
types
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Manually delete the keys from the state object to hide them.
|
||||||
|
const basicTypes = store.state['type-map'].basicTypes[productName];
|
||||||
|
|
||||||
|
if (basicTypes) {
|
||||||
|
types.forEach((t) => delete basicTypes[t]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kickSideNav();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start polling to check if the store is ready.
|
||||||
|
let attempts = 0;
|
||||||
|
const MAX_ATTEMPTS = 60;
|
||||||
|
|
||||||
|
const waitForStore = setInterval(() => {
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if the Schema definitions are loaded.
|
||||||
|
const hasSchema = store.getters[`${ productName }/schemaFor`] &&
|
||||||
|
store.getters[`${ productName }/schemaFor`](resourceType);
|
||||||
|
|
||||||
|
// Check if the resource list data is fully loaded to prevent race conditions.
|
||||||
|
const hasData = store.getters[`${ productName }/haveAll`] &&
|
||||||
|
store.getters[`${ productName }/haveAll`](resourceType);
|
||||||
|
|
||||||
|
if (hasSchema && hasData) {
|
||||||
|
// Store is ready. Stop polling.
|
||||||
|
clearInterval(waitForStore);
|
||||||
|
|
||||||
|
// Watch the specific addon resource for changes to its enabled status.
|
||||||
|
store.watch(
|
||||||
|
(state, getters) => {
|
||||||
|
const addons = getters[`${ productName }/all`](resourceType);
|
||||||
|
const addon = addons.find((a) => a.metadata.name === addonName);
|
||||||
|
|
||||||
|
return addon?.spec?.enabled === true;
|
||||||
|
},
|
||||||
|
(isEnabled) => {
|
||||||
|
setMenuVisibility(isEnabled);
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
);
|
||||||
|
} else if (hasSchema && !hasData) {
|
||||||
|
// If the schema is ready but the data is missing, request the list from the API.
|
||||||
|
// Ensures the script does not wait indefinitely if the UI has not loaded the addons yet.
|
||||||
|
store.dispatch(`${ productName }/findAll`, { type: resourceType });
|
||||||
|
} else if (attempts >= MAX_ATTEMPTS) {
|
||||||
|
// Stop checking if the store does not load within the timeout limit.
|
||||||
|
clearInterval(waitForStore);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore errors if the store module is not yet registered and wait for the next attempt.
|
||||||
|
if (attempts >= MAX_ATTEMPTS) clearInterval(waitForStore);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user