mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-08-16 12:49:14 +00:00
feat(forklift): UI based on the FIGMA
Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
This commit is contained in:
parent
7d8d74ce17
commit
2259910be1
53
pkg/harvester/components/MappingsCell.vue
Normal file
53
pkg/harvester/components/MappingsCell.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
networkEntries: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
storageEntries: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mappings-cell">
|
||||
<div
|
||||
v-for="(entry, idx) in networkEntries"
|
||||
:key="'net-' + idx"
|
||||
class="mapping-entry"
|
||||
>
|
||||
<i class="icon icon-cluster" />
|
||||
<span>{{ entry }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="(entry, idx) in storageEntries"
|
||||
:key="'stor-' + idx"
|
||||
class="mapping-entry"
|
||||
>
|
||||
<i class="icon icon-storage" />
|
||||
<span>{{ entry }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mappings-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mapping-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1341,10 +1341,8 @@ export function init($plugin, store) {
|
||||
AGE
|
||||
]);
|
||||
configureType(HCI.FORKLIFT_PROVIDER, {
|
||||
resource: HCI.FORKLIFT_PROVIDER,
|
||||
resourceDetail: HCI.FORKLIFT_PROVIDER,
|
||||
resourceEdit: HCI.FORKLIFT_PROVIDER,
|
||||
location: {
|
||||
resource: HCI.FORKLIFT_PROVIDER,
|
||||
location: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.FORKLIFT_PROVIDER }
|
||||
}
|
||||
@ -1352,7 +1350,7 @@ export function init($plugin, store) {
|
||||
virtualType({
|
||||
name: HCI.FORKLIFT_PROVIDER,
|
||||
labelKey: 'harvester.addons.forklift.labels.provider',
|
||||
group: 'forklift',
|
||||
group: 'forklift::Advanced',
|
||||
namespaced: true,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
@ -1370,10 +1368,8 @@ export function init($plugin, store) {
|
||||
AGE
|
||||
]);
|
||||
configureType(HCI.FORKLIFT_NETWORK_MAP, {
|
||||
resource: HCI.FORKLIFT_NETWORK_MAP,
|
||||
resourceDetail: HCI.FORKLIFT_NETWORK_MAP,
|
||||
resourceEdit: HCI.FORKLIFT_NETWORK_MAP,
|
||||
location: {
|
||||
resource: HCI.FORKLIFT_NETWORK_MAP,
|
||||
location: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.FORKLIFT_NETWORK_MAP }
|
||||
}
|
||||
@ -1381,7 +1377,7 @@ export function init($plugin, store) {
|
||||
virtualType({
|
||||
name: HCI.FORKLIFT_NETWORK_MAP,
|
||||
labelKey: 'harvester.addons.forklift.labels.networkMap',
|
||||
group: 'forklift',
|
||||
group: 'forklift::Advanced',
|
||||
namespaced: true,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
@ -1399,10 +1395,8 @@ export function init($plugin, store) {
|
||||
AGE
|
||||
]);
|
||||
configureType(HCI.FORKLIFT_STORAGE_MAP, {
|
||||
resource: HCI.FORKLIFT_STORAGE_MAP,
|
||||
resourceDetail: HCI.FORKLIFT_STORAGE_MAP,
|
||||
resourceEdit: HCI.FORKLIFT_STORAGE_MAP,
|
||||
location: {
|
||||
resource: HCI.FORKLIFT_STORAGE_MAP,
|
||||
location: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.FORKLIFT_STORAGE_MAP }
|
||||
}
|
||||
@ -1410,7 +1404,7 @@ export function init($plugin, store) {
|
||||
virtualType({
|
||||
name: HCI.FORKLIFT_STORAGE_MAP,
|
||||
labelKey: 'harvester.addons.forklift.labels.storageMap',
|
||||
group: 'forklift',
|
||||
group: 'forklift::Advanced',
|
||||
namespaced: true,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
@ -1429,10 +1423,8 @@ export function init($plugin, store) {
|
||||
AGE
|
||||
]);
|
||||
configureType(HCI.FORKLIFT_PLAN, {
|
||||
resource: HCI.FORKLIFT_PLAN,
|
||||
resourceDetail: HCI.FORKLIFT_PLAN,
|
||||
resourceEdit: HCI.FORKLIFT_PLAN,
|
||||
location: {
|
||||
resource: HCI.FORKLIFT_PLAN,
|
||||
location: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.FORKLIFT_PLAN }
|
||||
}
|
||||
@ -1440,7 +1432,7 @@ export function init($plugin, store) {
|
||||
virtualType({
|
||||
name: HCI.FORKLIFT_PLAN,
|
||||
labelKey: 'harvester.addons.forklift.labels.plan',
|
||||
group: 'forklift',
|
||||
group: 'forklift::Advanced',
|
||||
namespaced: true,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
@ -1457,10 +1449,8 @@ export function init($plugin, store) {
|
||||
AGE
|
||||
]);
|
||||
configureType(HCI.FORKLIFT_MIGRATION, {
|
||||
resource: HCI.FORKLIFT_MIGRATION,
|
||||
resourceDetail: HCI.FORKLIFT_MIGRATION,
|
||||
resourceEdit: HCI.FORKLIFT_MIGRATION,
|
||||
location: {
|
||||
resource: HCI.FORKLIFT_MIGRATION,
|
||||
location: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.FORKLIFT_MIGRATION }
|
||||
}
|
||||
@ -1468,19 +1458,38 @@ export function init($plugin, store) {
|
||||
virtualType({
|
||||
name: HCI.FORKLIFT_MIGRATION,
|
||||
labelKey: 'harvester.addons.forklift.labels.migration',
|
||||
group: 'forklift',
|
||||
group: 'forklift::Advanced',
|
||||
namespaced: true,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.FORKLIFT_MIGRATION }
|
||||
}
|
||||
});
|
||||
configureType('forklift-create', { subTypes: [HCI.FORKLIFT_PLAN] });
|
||||
virtualType({
|
||||
name: 'forklift-create',
|
||||
labelKey: 'harvester.addons.forklift.labels.plan',
|
||||
group: 'forklift',
|
||||
namespaced: true,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
||||
params: {}
|
||||
}
|
||||
});
|
||||
|
||||
// Enable SideNav based on Forklift Addon Status
|
||||
registerAddonSideNav(store, PRODUCT_NAME, {
|
||||
addonName: ADD_ONS.FORKLIFT_OPERATOR,
|
||||
resourceType: HCI.ADD_ONS,
|
||||
navGroup: 'forklift',
|
||||
types: [
|
||||
'forklift-create',
|
||||
]
|
||||
});
|
||||
registerAddonSideNav(store, PRODUCT_NAME, {
|
||||
addonName: ADD_ONS.FORKLIFT_OPERATOR,
|
||||
resourceType: HCI.ADD_ONS,
|
||||
navGroup: 'forklift::Advanced',
|
||||
types: [
|
||||
HCI.FORKLIFT_PROVIDER,
|
||||
HCI.FORKLIFT_NETWORK_MAP,
|
||||
|
||||
@ -286,7 +286,7 @@ export const FORKLIFT_PLAN_VM_COUNT = {
|
||||
labelKey: 'harvester.tableHeaders.forkliftPlanVmCount',
|
||||
value: 'spec.vms.length',
|
||||
sort: 'spec.vms.length',
|
||||
align: 'center',
|
||||
align: 'left',
|
||||
};
|
||||
|
||||
// Plan reference column in forklift.konveyor.io.migration list page
|
||||
|
||||
@ -1,383 +0,0 @@
|
||||
<script>
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import { Banner } from '@components/Banner';
|
||||
import PercentageBar from '@shell/components/PercentageBar';
|
||||
import BadgeState from '@components/BadgeState';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'ForkliftMigrationDetail',
|
||||
|
||||
components: {
|
||||
BadgeState,
|
||||
Banner,
|
||||
CruResource,
|
||||
LabelValue,
|
||||
PercentageBar,
|
||||
Tabbed,
|
||||
Tab,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
planName() {
|
||||
return this.value.spec?.plan?.name || '-';
|
||||
},
|
||||
|
||||
planNamespace() {
|
||||
return this.value.spec?.plan?.namespace || '-';
|
||||
},
|
||||
|
||||
migrationStarted() {
|
||||
return this.value.status?.started || '-';
|
||||
},
|
||||
|
||||
migrationCompleted() {
|
||||
return this.value.status?.completed || '-';
|
||||
},
|
||||
|
||||
migrationConditions() {
|
||||
return this.value.status?.conditions || [];
|
||||
},
|
||||
|
||||
overallStatus() {
|
||||
const succeeded = this.migrationConditions.find((c) => c.type === 'Succeeded' && c.status === 'True');
|
||||
const running = this.migrationConditions.find((c) => c.type === 'Running' && c.status === 'True');
|
||||
const failed = this.migrationConditions.find((c) => c.type === 'Failed' && c.status === 'True');
|
||||
|
||||
if (succeeded) {
|
||||
return { text: this.t('harvester.addons.forklift.detail.status.succeeded'), color: 'bg-success' };
|
||||
}
|
||||
if (failed) {
|
||||
return { text: this.t('harvester.addons.forklift.detail.status.failed'), color: 'bg-error' };
|
||||
}
|
||||
if (running) {
|
||||
return { text: this.t('harvester.addons.forklift.detail.status.running'), color: 'bg-info' };
|
||||
}
|
||||
|
||||
return { text: this.t('harvester.addons.forklift.detail.status.pending'), color: 'bg-darker' };
|
||||
},
|
||||
|
||||
vms() {
|
||||
return this.value.status?.vms || [];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
pipelineSteps(vm) {
|
||||
return vm.pipeline || [];
|
||||
},
|
||||
|
||||
stepProgress(step) {
|
||||
if (step.phase === 'Completed') {
|
||||
return 100;
|
||||
}
|
||||
if (step.phase === 'Pending') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const total = step.progress?.total || 0;
|
||||
const completed = step.progress?.completed || 0;
|
||||
|
||||
if (total === 0) {
|
||||
return step.phase === 'Running' ? 50 : 0;
|
||||
}
|
||||
|
||||
return Math.round((completed / total) * 100);
|
||||
},
|
||||
|
||||
stepBadgeColor(phase) {
|
||||
switch (phase) {
|
||||
case 'Completed':
|
||||
return 'bg-success';
|
||||
case 'Running':
|
||||
return 'bg-info';
|
||||
case 'Pending':
|
||||
return 'bg-darker';
|
||||
default:
|
||||
return 'bg-warning';
|
||||
}
|
||||
},
|
||||
|
||||
vmOverallProgress(vm) {
|
||||
const steps = vm.pipeline || [];
|
||||
|
||||
if (steps.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const completed = steps.filter((s) => s.phase === 'Completed').length;
|
||||
|
||||
return Math.round((completed / steps.length) * 100);
|
||||
},
|
||||
|
||||
formatBytes(mb) {
|
||||
if (mb >= 1024) {
|
||||
return `${ (mb / 1024).toFixed(1) } GB`;
|
||||
}
|
||||
|
||||
return `${ mb } MB`;
|
||||
},
|
||||
|
||||
formatDiskProgress(step) {
|
||||
const unit = step.annotations?.unit || 'MB';
|
||||
const completed = step.progress?.completed || 0;
|
||||
const total = step.progress?.total || 0;
|
||||
|
||||
if (unit === 'MB') {
|
||||
return `${ this.formatBytes(completed) } / ${ this.formatBytes(total) }`;
|
||||
}
|
||||
|
||||
return `${ completed } / ${ total }`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CruResource
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:can-yaml="false"
|
||||
>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.addons.forklift.detail.plan')"
|
||||
:value="planName"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.addons.forklift.detail.planNamespace')"
|
||||
:value="planNamespace"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabelValue :name="t('harvester.addons.forklift.detail.status.label')">
|
||||
<template #value>
|
||||
<BadgeState
|
||||
:label="overallStatus.text"
|
||||
:color="overallStatus.color"
|
||||
/>
|
||||
</template>
|
||||
</LabelValue>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.addons.forklift.detail.started')"
|
||||
:value="migrationStarted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabbed
|
||||
v-bind="$attrs"
|
||||
class="mt-15"
|
||||
:side-tabs="true"
|
||||
>
|
||||
<Tab
|
||||
v-for="(vm, vmIdx) in vms"
|
||||
:key="vm.id"
|
||||
:name="'vm-' + vmIdx"
|
||||
:label="vm.name || vm.id"
|
||||
:weight="100 - vmIdx"
|
||||
>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.addons.forklift.detail.vmId')"
|
||||
:value="vm.id"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabelValue :name="t('harvester.addons.forklift.detail.vmPhase')">
|
||||
<template #value>
|
||||
<BadgeState
|
||||
:label="vm.phase"
|
||||
:color="stepBadgeColor(vm.phase)"
|
||||
/>
|
||||
</template>
|
||||
</LabelValue>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.addons.forklift.detail.vmStarted')"
|
||||
:value="vm.started || '-'"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.addons.forklift.detail.vmCompleted')"
|
||||
:value="vm.completed || '-'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-12">
|
||||
<LabelValue :name="t('harvester.addons.forklift.detail.overallProgress')">
|
||||
<template #value>
|
||||
<PercentageBar :value="vmOverallProgress(vm)" />
|
||||
</template>
|
||||
</LabelValue>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="mb-10">
|
||||
{{ t('harvester.addons.forklift.detail.pipelineSteps') }}
|
||||
</h3>
|
||||
|
||||
<div class="pipeline">
|
||||
<div
|
||||
v-for="(step, stepIdx) in pipelineSteps(vm)"
|
||||
:key="stepIdx"
|
||||
class="pipeline-step"
|
||||
>
|
||||
<div class="step-header">
|
||||
<div class="step-name">
|
||||
<span class="step-index">{{ stepIdx + 1 }}.</span>
|
||||
{{ step.name }}
|
||||
</div>
|
||||
<BadgeState
|
||||
:label="step.phase"
|
||||
:color="stepBadgeColor(step.phase)"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-muted step-description">
|
||||
{{ step.description }}
|
||||
</p>
|
||||
|
||||
<PercentageBar
|
||||
:value="stepProgress(step)"
|
||||
class="mb-5"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="step.annotations && step.annotations.unit"
|
||||
class="step-disk-progress text-muted"
|
||||
>
|
||||
{{ formatDiskProgress(step) }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="step.tasks && step.tasks.length"
|
||||
class="step-tasks mt-10"
|
||||
>
|
||||
<div
|
||||
v-for="(task, taskIdx) in step.tasks"
|
||||
:key="taskIdx"
|
||||
class="task-row"
|
||||
>
|
||||
<span class="task-name text-muted">{{ task.name }}</span>
|
||||
<span
|
||||
v-if="task.progress"
|
||||
class="task-progress text-muted"
|
||||
>
|
||||
{{ formatBytes(task.progress.completed || 0) }} / {{ formatBytes(task.progress.total || 0) }}
|
||||
</span>
|
||||
<BadgeState
|
||||
v-if="task.phase"
|
||||
:label="task.phase"
|
||||
:color="stepBadgeColor(task.phase)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Banner
|
||||
v-if="vm.conditions && vm.conditions.length"
|
||||
:color="vm.conditions[0].type === 'Succeeded' ? 'success' : 'info'"
|
||||
:label="vm.conditions[0].message"
|
||||
class="mt-20"
|
||||
/>
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</CruResource>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pipeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.pipeline-step {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.step-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.step-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.step-index {
|
||||
color: var(--muted);
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.step-description {
|
||||
font-size: 13px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.step-disk-progress {
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.step-tasks {
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.task-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 4px 0;
|
||||
font-size: 12px;
|
||||
|
||||
.task-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-progress {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,541 +0,0 @@
|
||||
<script>
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import { Banner } from '@components/Banner';
|
||||
import PercentageBar from '@shell/components/PercentageBar';
|
||||
import BadgeState from '@components/BadgeState';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'ForkliftPlanDetail',
|
||||
|
||||
components: {
|
||||
BadgeState,
|
||||
Banner,
|
||||
CruResource,
|
||||
LabelValue,
|
||||
PercentageBar,
|
||||
Tabbed,
|
||||
Tab,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
sourceProvider() {
|
||||
return this.value.spec?.provider?.source?.name || '-';
|
||||
},
|
||||
|
||||
destProvider() {
|
||||
return this.value.spec?.provider?.destination?.name || '-';
|
||||
},
|
||||
|
||||
targetNamespace() {
|
||||
return this.value.spec?.targetNamespace || '-';
|
||||
},
|
||||
|
||||
networkMapName() {
|
||||
return this.value.spec?.map?.network?.name || '-';
|
||||
},
|
||||
|
||||
storageMapName() {
|
||||
return this.value.spec?.map?.storage?.name || '-';
|
||||
},
|
||||
|
||||
plannedVMs() {
|
||||
return this.value.spec?.vms || [];
|
||||
},
|
||||
|
||||
planConditions() {
|
||||
return this.value.status?.conditions || [];
|
||||
},
|
||||
|
||||
overallStatus() {
|
||||
const succeeded = this.planConditions.find((c) => c.type === 'Succeeded' && c.status === 'True');
|
||||
const executing = this.planConditions.find((c) => c.type === 'Executing' && c.status === 'True');
|
||||
const failed = this.planConditions.find((c) => c.type === 'Failed' && c.status === 'True');
|
||||
|
||||
if (succeeded) {
|
||||
return { text: this.t('harvester.addons.forklift.detail.status.succeeded'), color: 'bg-success' };
|
||||
}
|
||||
if (failed) {
|
||||
return { text: this.t('harvester.addons.forklift.detail.status.failed'), color: 'bg-error' };
|
||||
}
|
||||
if (executing) {
|
||||
return { text: this.t('harvester.addons.forklift.detail.status.executing'), color: 'bg-info' };
|
||||
}
|
||||
|
||||
return { text: this.t('harvester.addons.forklift.detail.status.ready'), color: 'bg-darker' };
|
||||
},
|
||||
|
||||
currentMigration() {
|
||||
return this.value.status?.migration || null;
|
||||
},
|
||||
|
||||
currentMigrationVMs() {
|
||||
return this.currentMigration?.vms || [];
|
||||
},
|
||||
|
||||
migrationHistory() {
|
||||
return this.currentMigration?.history || [];
|
||||
},
|
||||
|
||||
hasMigrationInProgress() {
|
||||
return this.currentMigration?.started && !this.currentMigration?.completed;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
vmOverallProgress(vm) {
|
||||
const steps = vm.pipeline || [];
|
||||
|
||||
if (steps.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const completed = steps.filter((s) => s.phase === 'Completed').length;
|
||||
|
||||
return Math.round((completed / steps.length) * 100);
|
||||
},
|
||||
|
||||
stepProgress(step) {
|
||||
if (step.phase === 'Completed') {
|
||||
return 100;
|
||||
}
|
||||
if (step.phase === 'Pending') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const total = step.progress?.total || 0;
|
||||
const completed = step.progress?.completed || 0;
|
||||
|
||||
if (total === 0) {
|
||||
return step.phase === 'Running' ? 50 : 0;
|
||||
}
|
||||
|
||||
return Math.round((completed / total) * 100);
|
||||
},
|
||||
|
||||
stepBadgeColor(phase) {
|
||||
switch (phase) {
|
||||
case 'Completed':
|
||||
return 'bg-success';
|
||||
case 'Running':
|
||||
return 'bg-info';
|
||||
case 'Pending':
|
||||
return 'bg-darker';
|
||||
default:
|
||||
return 'bg-warning';
|
||||
}
|
||||
},
|
||||
|
||||
formatBytes(mb) {
|
||||
if (mb >= 1024) {
|
||||
return `${ (mb / 1024).toFixed(1) } GB`;
|
||||
}
|
||||
|
||||
return `${ mb } MB`;
|
||||
},
|
||||
|
||||
formatDiskProgress(step) {
|
||||
const unit = step.annotations?.unit || 'MB';
|
||||
const completed = step.progress?.completed || 0;
|
||||
const total = step.progress?.total || 0;
|
||||
|
||||
if (unit === 'MB') {
|
||||
return `${ this.formatBytes(completed) } / ${ this.formatBytes(total) }`;
|
||||
}
|
||||
|
||||
return `${ completed } / ${ total }`;
|
||||
},
|
||||
|
||||
historyStatus(entry) {
|
||||
const conditions = entry.conditions || [];
|
||||
const succeeded = conditions.find((c) => c.type === 'Succeeded');
|
||||
const executing = conditions.find((c) => c.type === 'Executing');
|
||||
const failed = conditions.find((c) => c.type === 'Failed');
|
||||
|
||||
if (succeeded) {
|
||||
return { text: this.t('harvester.addons.forklift.detail.status.succeeded'), color: 'bg-success' };
|
||||
}
|
||||
if (failed) {
|
||||
return { text: this.t('harvester.addons.forklift.detail.status.failed'), color: 'bg-error' };
|
||||
}
|
||||
if (executing) {
|
||||
return { text: this.t('harvester.addons.forklift.detail.status.executing'), color: 'bg-info' };
|
||||
}
|
||||
|
||||
return { text: this.t('harvester.addons.forklift.detail.status.unknown'), color: 'bg-darker' };
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CruResource
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:can-yaml="false"
|
||||
>
|
||||
<!-- Plan Overview -->
|
||||
<div class="row mb-20">
|
||||
<div class="col span-3">
|
||||
<LabelValue :name="t('harvester.addons.forklift.detail.status.label')">
|
||||
<template #value>
|
||||
<BadgeState
|
||||
:label="overallStatus.text"
|
||||
:color="overallStatus.color"
|
||||
/>
|
||||
</template>
|
||||
</LabelValue>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.addons.forklift.fields.sourceProvider')"
|
||||
:value="sourceProvider"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.addons.forklift.fields.destProvider')"
|
||||
:value="destProvider"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.addons.forklift.fields.targetNamespace')"
|
||||
:value="targetNamespace"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.addons.forklift.fields.networkMap')"
|
||||
:value="networkMapName"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.addons.forklift.fields.storageMap')"
|
||||
:value="storageMapName"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.addons.forklift.detail.vmCount')"
|
||||
:value="String(plannedVMs.length)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabbed
|
||||
v-bind="$attrs"
|
||||
class="mt-15"
|
||||
:side-tabs="true"
|
||||
>
|
||||
<!-- Current Migration Tab -->
|
||||
<Tab
|
||||
name="current"
|
||||
:label="t('harvester.addons.forklift.detail.tabs.currentMigration')"
|
||||
:weight="3"
|
||||
>
|
||||
<template v-if="currentMigration">
|
||||
<div class="row mb-20">
|
||||
<div class="col span-4">
|
||||
<LabelValue
|
||||
:name="t('harvester.addons.forklift.detail.migrationName')"
|
||||
:value="currentMigration.history && currentMigration.history.length ? currentMigration.history[currentMigration.history.length - 1].migration.name : '-'"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<LabelValue
|
||||
:name="t('harvester.addons.forklift.detail.started')"
|
||||
:value="currentMigration.started || '-'"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<LabelValue
|
||||
:name="t('harvester.addons.forklift.detail.completed')"
|
||||
:value="currentMigration.completed || '-'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VM Progress -->
|
||||
<div
|
||||
v-for="vm in currentMigrationVMs"
|
||||
:key="vm.id"
|
||||
class="vm-progress-card mb-20"
|
||||
>
|
||||
<div class="vm-card-header">
|
||||
<div class="vm-card-title">
|
||||
<span class="vm-name">{{ vm.name || vm.id }}</span>
|
||||
<span
|
||||
v-if="vm.name"
|
||||
class="text-muted ml-10"
|
||||
>({{ vm.id }})</span>
|
||||
</div>
|
||||
<BadgeState
|
||||
:label="vm.phase"
|
||||
:color="stepBadgeColor(vm.phase)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row mb-10">
|
||||
<div class="col span-12">
|
||||
<PercentageBar :value="vmOverallProgress(vm)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pipeline-steps">
|
||||
<div
|
||||
v-for="(step, idx) in (vm.pipeline || [])"
|
||||
:key="idx"
|
||||
class="step-row"
|
||||
>
|
||||
<div class="step-info">
|
||||
<span class="step-num">{{ idx + 1 }}.</span>
|
||||
<span class="step-name">{{ step.name }}</span>
|
||||
<span class="text-muted step-desc">{{ step.description }}</span>
|
||||
</div>
|
||||
<div class="step-status">
|
||||
<span
|
||||
v-if="step.annotations && step.annotations.unit"
|
||||
class="text-muted step-data mr-10"
|
||||
>
|
||||
{{ formatDiskProgress(step) }}
|
||||
</span>
|
||||
<BadgeState
|
||||
:label="step.phase"
|
||||
:color="stepBadgeColor(step.phase)"
|
||||
/>
|
||||
</div>
|
||||
<PercentageBar
|
||||
:value="stepProgress(step)"
|
||||
class="step-bar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Banner
|
||||
v-if="vm.conditions && vm.conditions.length"
|
||||
:color="vm.conditions[0].type === 'Succeeded' ? 'success' : 'info'"
|
||||
:label="vm.conditions[0].message"
|
||||
class="mt-10"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Banner
|
||||
v-else
|
||||
color="info"
|
||||
:label="t('harvester.addons.forklift.detail.noCurrentMigration')"
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<!-- Migration History Tab -->
|
||||
<Tab
|
||||
name="history"
|
||||
:label="t('harvester.addons.forklift.detail.tabs.history')"
|
||||
:weight="2"
|
||||
>
|
||||
<template v-if="migrationHistory.length">
|
||||
<div
|
||||
v-for="(entry, idx) in migrationHistory"
|
||||
:key="idx"
|
||||
class="history-entry mb-15"
|
||||
>
|
||||
<div class="history-header">
|
||||
<div>
|
||||
<strong>{{ entry.migration?.name || t('harvester.addons.forklift.detail.migrationRun', { idx: idx + 1 }) }}</strong>
|
||||
</div>
|
||||
<BadgeState
|
||||
:label="historyStatus(entry).text"
|
||||
:color="historyStatus(entry).color"
|
||||
/>
|
||||
</div>
|
||||
<div class="history-details">
|
||||
<div class="row">
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.addons.forklift.fields.sourceProvider')"
|
||||
:value="entry.provider?.source?.name || '-'"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.addons.forklift.fields.destProvider')"
|
||||
:value="entry.provider?.destination?.name || '-'"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.addons.forklift.fields.networkMap')"
|
||||
:value="entry.map?.network?.name || '-'"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.addons.forklift.fields.storageMap')"
|
||||
:value="entry.map?.storage?.name || '-'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Banner
|
||||
v-if="entry.conditions && entry.conditions.length"
|
||||
:color="entry.conditions[0].type === 'Succeeded' ? 'success' : entry.conditions[0].type === 'Failed' ? 'error' : 'info'"
|
||||
:label="entry.conditions[0].message"
|
||||
class="mt-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Banner
|
||||
v-else
|
||||
color="info"
|
||||
:label="t('harvester.addons.forklift.detail.noHistory')"
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<!-- Planned VMs Tab -->
|
||||
<Tab
|
||||
name="vms"
|
||||
:label="t('harvester.addons.forklift.titles.vms')"
|
||||
:weight="1"
|
||||
>
|
||||
<table class="sortable-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ t('harvester.addons.forklift.fields.vmId') }}</th>
|
||||
<th>{{ t('harvester.addons.forklift.fields.vmName') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(vm, idx) in plannedVMs"
|
||||
:key="idx"
|
||||
>
|
||||
<td>{{ vm.id }}</td>
|
||||
<td>{{ vm.name || '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</CruResource>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vm-progress-card {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.vm-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.vm-card-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pipeline-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.step-row {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.step-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.step-num {
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.step-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.step-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.step-data {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.step-bar {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.history-entry {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.history-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sortable-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
th, td {
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,119 +0,0 @@
|
||||
<script>
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import FormValidation from '@shell/mixins/form-validation';
|
||||
import { HCI } from '../types';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'EditForkliftMigration',
|
||||
|
||||
emits: ['update:value'],
|
||||
|
||||
components: {
|
||||
CruResource,
|
||||
LabeledSelect,
|
||||
NameNsDescription,
|
||||
},
|
||||
|
||||
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.allPlans = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PLAN });
|
||||
},
|
||||
|
||||
data() {
|
||||
if (!this.value.spec) {
|
||||
this.value.spec = { plan: {} };
|
||||
}
|
||||
|
||||
return {
|
||||
allPlans: [],
|
||||
|
||||
fvFormRuleSets: [
|
||||
{ path: 'metadata.name', rules: ['nameRequired'] },
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
planOptions() {
|
||||
return this.allPlans.map((p) => ({
|
||||
label: p.metadata.name,
|
||||
value: p.metadata.name,
|
||||
}));
|
||||
},
|
||||
|
||||
fvExtraRules() {
|
||||
return { nameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.fields.name') }) : undefined };
|
||||
},
|
||||
|
||||
isFormValid() {
|
||||
return this.fvFormIsValid && !!this.value.spec.plan?.name;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
updatePlan(val) {
|
||||
const plan = this.allPlans.find((p) => p.metadata.name === val);
|
||||
|
||||
this.value.spec.plan = {
|
||||
name: val,
|
||||
namespace: plan?.metadata?.namespace || this.value.metadata.namespace || 'default',
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CruResource
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:errors="errors"
|
||||
:apply-hooks="applyHooks"
|
||||
:validation-passed="isFormValid"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
:mode="mode"
|
||||
:rules="{ name: fvGetAndReportPathRules('metadata.name') }"
|
||||
@update:value="$emit('update:value', $event)"
|
||||
/>
|
||||
|
||||
<div class="row mb-20 mt-20">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
:value="value.spec.plan.name"
|
||||
:options="planOptions"
|
||||
:label="t('harvester.addons.forklift.fields.plan')"
|
||||
:mode="mode"
|
||||
required
|
||||
@update:value="updatePlan"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CruResource>
|
||||
</template>
|
||||
@ -1,199 +0,0 @@
|
||||
<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 CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import FormValidation from '@shell/mixins/form-validation';
|
||||
import ForkliftProvider from '../mixins/forklift-provider';
|
||||
import { HCI } from '../types';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'EditForkliftNetworkMap',
|
||||
|
||||
emits: ['update:value'],
|
||||
|
||||
components: {
|
||||
CruResource,
|
||||
Tabbed,
|
||||
Tab,
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
NameNsDescription,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView, FormValidation, ForkliftProvider],
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
this.allProviders = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
|
||||
},
|
||||
|
||||
data() {
|
||||
if (!this.value.spec) {
|
||||
this.value.spec = {
|
||||
map: [{ source: { id: '' }, destination: { type: 'pod' } }],
|
||||
provider: {
|
||||
source: {},
|
||||
destination: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
fvFormRuleSets: [
|
||||
{ path: 'metadata.name', rules: ['nameRequired'] },
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
destinationTypeOptions() {
|
||||
return [
|
||||
{ label: this.t('harvester.addons.forklift.fields.destNetworkPod'), value: 'pod' },
|
||||
{ label: this.t('harvester.addons.forklift.fields.destNetworkMultus'), value: 'multus' },
|
||||
];
|
||||
},
|
||||
|
||||
fvExtraRules() {
|
||||
return { nameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.fields.name') }) : undefined };
|
||||
},
|
||||
|
||||
isFormValid() {
|
||||
return this.fvFormIsValid &&
|
||||
!!this.value.spec.provider?.source?.name &&
|
||||
!!this.value.spec.provider?.destination?.name;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
addMapping() {
|
||||
this.value.spec.map.push({ source: { id: '' }, destination: { type: 'pod' } });
|
||||
},
|
||||
|
||||
removeMapping(idx) {
|
||||
this.value.spec.map.splice(idx, 1);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CruResource
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:errors="errors"
|
||||
:apply-hooks="applyHooks"
|
||||
:validation-passed="isFormValid"
|
||||
@finish="save"
|
||||
@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="providers"
|
||||
:label="t('harvester.addons.forklift.titles.providers')"
|
||||
:weight="3"
|
||||
>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
:value="value.spec.provider.source.name"
|
||||
:options="providerOptions"
|
||||
:label="t('harvester.addons.forklift.fields.sourceProvider')"
|
||||
:mode="mode"
|
||||
required
|
||||
@update:value="updateSourceProvider"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
:value="value.spec.provider.destination.name"
|
||||
:options="providerOptions"
|
||||
:label="t('harvester.addons.forklift.fields.destProvider')"
|
||||
:mode="mode"
|
||||
required
|
||||
@update:value="updateDestProvider"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="mappings"
|
||||
:label="t('harvester.addons.forklift.titles.networkMappings')"
|
||||
:weight="2"
|
||||
>
|
||||
<div
|
||||
v-for="(entry, idx) in value.spec.map"
|
||||
:key="idx"
|
||||
class="row mb-20"
|
||||
>
|
||||
<div class="col span-5">
|
||||
<LabeledInput
|
||||
v-model:value="entry.source.id"
|
||||
:label="t('harvester.addons.forklift.fields.sourceNetworkId')"
|
||||
:placeholder="t('harvester.addons.forklift.placeholders.sourceNetworkId')"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-5">
|
||||
<LabeledSelect
|
||||
v-model:value="entry.destination.type"
|
||||
:options="destinationTypeOptions"
|
||||
:label="t('harvester.addons.forklift.fields.destNetworkType')"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn role-link"
|
||||
:disabled="value.spec.map.length <= 1"
|
||||
@click="removeMapping(idx)"
|
||||
>
|
||||
{{ t('harvester.addons.forklift.actions.remove') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn role-tertiary"
|
||||
@click="addMapping"
|
||||
>
|
||||
{{ t('harvester.addons.forklift.actions.addMapping') }}
|
||||
</button>
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</CruResource>
|
||||
</template>
|
||||
@ -1,303 +0,0 @@
|
||||
<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 ForkliftProvider from '../mixins/forklift-provider';
|
||||
import { HCI } from '../types';
|
||||
import { FORKLIFT_API_VERSION } from '../config/harvester-map';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'EditForkliftPlan',
|
||||
|
||||
emits: ['update:value'],
|
||||
|
||||
components: {
|
||||
CruResource,
|
||||
Tabbed,
|
||||
Tab,
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
NameNsDescription,
|
||||
Checkbox,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView, FormValidation, ForkliftProvider],
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
this.allProviders = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
|
||||
this.allNetworkMaps = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_NETWORK_MAP });
|
||||
this.allStorageMaps = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_STORAGE_MAP });
|
||||
},
|
||||
|
||||
data() {
|
||||
if (!this.value.spec) {
|
||||
this.value.spec = {
|
||||
map: {
|
||||
network: {},
|
||||
storage: {},
|
||||
},
|
||||
provider: {
|
||||
source: {},
|
||||
destination: {},
|
||||
},
|
||||
targetNamespace: 'default',
|
||||
migrateSharedDisks: true,
|
||||
vms: [{ id: '', name: '' }],
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.value.metadata.annotations) {
|
||||
this.value.metadata.annotations = {};
|
||||
}
|
||||
this.value.metadata.annotations.populatorLabels = 'True';
|
||||
|
||||
return {
|
||||
allProviders: [],
|
||||
allNetworkMaps: [],
|
||||
allStorageMaps: [],
|
||||
|
||||
fvFormRuleSets: [
|
||||
{ path: 'metadata.name', rules: ['nameRequired'] },
|
||||
{ path: 'spec.targetNamespace', rules: ['targetNsRequired'] },
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
providerOptions() {
|
||||
return this.allProviders.map((p) => ({
|
||||
label: `${ p.metadata.name } (${ p.spec?.type || 'unknown' })`,
|
||||
value: p.metadata.name,
|
||||
}));
|
||||
},
|
||||
|
||||
networkMapOptions() {
|
||||
return this.allNetworkMaps.map((m) => ({
|
||||
label: m.metadata.name,
|
||||
value: m.metadata.name,
|
||||
}));
|
||||
},
|
||||
|
||||
storageMapOptions() {
|
||||
return this.allStorageMaps.map((m) => ({
|
||||
label: m.metadata.name,
|
||||
value: m.metadata.name,
|
||||
}));
|
||||
},
|
||||
|
||||
fvExtraRules() {
|
||||
return {
|
||||
nameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.fields.name') }) : undefined,
|
||||
targetNsRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.forklift.fields.targetNamespace') }) : undefined,
|
||||
};
|
||||
},
|
||||
|
||||
isFormValid() {
|
||||
return this.fvFormIsValid &&
|
||||
!!this.value.spec.provider?.source?.name &&
|
||||
!!this.value.spec.provider?.destination?.name;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
addVm() {
|
||||
this.value.spec.vms.push({ id: '', name: '' });
|
||||
},
|
||||
|
||||
removeVm(idx) {
|
||||
this.value.spec.vms.splice(idx, 1);
|
||||
},
|
||||
|
||||
updateNetworkMap(val) {
|
||||
this.value.spec.map.network = {
|
||||
apiVersion: FORKLIFT_API_VERSION,
|
||||
kind: 'NetworkMap',
|
||||
name: val,
|
||||
namespace: this.value.metadata.namespace || 'default',
|
||||
};
|
||||
},
|
||||
|
||||
updateStorageMap(val) {
|
||||
this.value.spec.map.storage = {
|
||||
apiVersion: FORKLIFT_API_VERSION,
|
||||
kind: 'StorageMap',
|
||||
name: val,
|
||||
namespace: this.value.metadata.namespace || 'default',
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CruResource
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:errors="errors"
|
||||
:apply-hooks="applyHooks"
|
||||
:validation-passed="isFormValid"
|
||||
@finish="save"
|
||||
@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="providers"
|
||||
:label="t('harvester.addons.forklift.titles.providers')"
|
||||
:weight="4"
|
||||
>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
:value="value.spec.provider.source.name"
|
||||
:options="providerOptions"
|
||||
:label="t('harvester.addons.forklift.fields.sourceProvider')"
|
||||
:mode="mode"
|
||||
required
|
||||
@update:value="updateSourceProvider"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
:value="value.spec.provider.destination.name"
|
||||
:options="providerOptions"
|
||||
:label="t('harvester.addons.forklift.fields.destProvider')"
|
||||
:mode="mode"
|
||||
required
|
||||
@update:value="updateDestProvider"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="mappings"
|
||||
:label="t('harvester.addons.forklift.titles.mappings')"
|
||||
:weight="3"
|
||||
>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
:value="value.spec.map.network.name"
|
||||
:options="networkMapOptions"
|
||||
:label="t('harvester.addons.forklift.fields.networkMap')"
|
||||
:mode="mode"
|
||||
required
|
||||
@update:value="updateNetworkMap"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
:value="value.spec.map.storage.name"
|
||||
:options="storageMapOptions"
|
||||
:label="t('harvester.addons.forklift.fields.storageMap')"
|
||||
:mode="mode"
|
||||
required
|
||||
@update:value="updateStorageMap"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="vms"
|
||||
:label="t('harvester.addons.forklift.titles.vms')"
|
||||
:weight="2"
|
||||
>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="value.spec.targetNamespace"
|
||||
:label="t('harvester.addons.forklift.fields.targetNamespace')"
|
||||
:mode="mode"
|
||||
:rules="fvGetAndReportPathRules('spec.targetNamespace')"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<Checkbox
|
||||
v-model:value="value.spec.migrateSharedDisks"
|
||||
:label="t('harvester.addons.forklift.fields.migrateSharedDisks')"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(vm, idx) in value.spec.vms"
|
||||
:key="idx"
|
||||
class="row mb-20"
|
||||
>
|
||||
<div class="col span-5">
|
||||
<LabeledInput
|
||||
v-model:value="vm.id"
|
||||
:label="t('harvester.addons.forklift.fields.vmId')"
|
||||
:placeholder="t('harvester.addons.forklift.placeholders.vmId')"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-5">
|
||||
<LabeledInput
|
||||
v-model:value="vm.name"
|
||||
:label="t('harvester.addons.forklift.fields.vmName')"
|
||||
:placeholder="t('harvester.addons.forklift.placeholders.vmName')"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn role-link"
|
||||
:disabled="value.spec.vms.length <= 1"
|
||||
@click="removeVm(idx)"
|
||||
>
|
||||
{{ t('harvester.addons.forklift.actions.remove') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn role-tertiary"
|
||||
@click="addVm"
|
||||
>
|
||||
{{ t('harvester.addons.forklift.actions.addVm') }}
|
||||
</button>
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</CruResource>
|
||||
</template>
|
||||
@ -1,346 +0,0 @@
|
||||
<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 { Checkbox } from '@components/Form/Checkbox';
|
||||
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';
|
||||
|
||||
const PROVIDER_TYPES = [
|
||||
{ labelKey: 'harvester.addons.forklift.fields.providerTypeVsphere', value: 'vsphere' },
|
||||
{ labelKey: 'harvester.addons.forklift.fields.providerTypeOpenstack', value: 'openstack' },
|
||||
{ labelKey: 'harvester.addons.forklift.fields.providerTypeOpenshift', value: 'openshift' },
|
||||
];
|
||||
|
||||
export default {
|
||||
name: 'EditForkliftProvider',
|
||||
|
||||
emits: ['update:value'],
|
||||
|
||||
components: {
|
||||
CruResource,
|
||||
Tabbed,
|
||||
Tab,
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
NameNsDescription,
|
||||
RadioGroup,
|
||||
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;
|
||||
|
||||
this.allSecrets = await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
|
||||
},
|
||||
|
||||
data() {
|
||||
if (!this.value.spec) {
|
||||
this.value.spec = {
|
||||
type: 'vsphere',
|
||||
url: '',
|
||||
secret: {},
|
||||
};
|
||||
}
|
||||
|
||||
const initialMode = this.value.spec.secret?.name ? 'existing' : 'new';
|
||||
|
||||
return {
|
||||
allSecrets: [],
|
||||
authMode: initialMode,
|
||||
newUsername: '',
|
||||
newPassword: '',
|
||||
newCaCert: '',
|
||||
skipTlsVerify: true,
|
||||
|
||||
fvFormRuleSets: [
|
||||
{ path: 'metadata.name', rules: ['nameRequired'] },
|
||||
{ path: 'spec.url', rules: ['urlRequired'] },
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
providerTypes() {
|
||||
return PROVIDER_TYPES.map((pt) => ({
|
||||
label: this.t(pt.labelKey),
|
||||
value: pt.value,
|
||||
}));
|
||||
},
|
||||
|
||||
authModeOptions() {
|
||||
return [
|
||||
{ label: this.t('harvester.addons.forklift.fields.createSecret'), value: 'new' },
|
||||
{ label: this.t('harvester.addons.forklift.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
|
||||
}));
|
||||
},
|
||||
|
||||
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.forklift.fields.url') }) : undefined,
|
||||
};
|
||||
},
|
||||
|
||||
isFormValid() {
|
||||
if (!this.fvFormIsValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Local openshift provider doesn't need auth or URL
|
||||
if (this.value.spec.type === 'openshift') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.authMode === 'new') {
|
||||
if (!this.newUsername || !this.newPassword) {
|
||||
return false;
|
||||
}
|
||||
} else if (!this.value.spec.secret?.name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
usernameRule(val) {
|
||||
return !val ? this.t('validation.required', { key: this.t('harvester.addons.forklift.fields.username') }) : undefined;
|
||||
},
|
||||
passwordRule(val) {
|
||||
return !val ? this.t('validation.required', { key: this.t('harvester.addons.forklift.fields.password') }) : undefined;
|
||||
},
|
||||
secretRule(val) {
|
||||
return !val ? this.t('validation.required', { key: this.t('harvester.addons.forklift.fields.selectSecret') }) : undefined;
|
||||
},
|
||||
|
||||
async saveProvider(buttonCb) {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
try {
|
||||
// Local openshift provider uses empty secret
|
||||
if (this.value.spec.type === 'openshift') {
|
||||
this.value.spec.secret = {};
|
||||
this.value.spec.url = '';
|
||||
await this.save(buttonCb);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
labels: {
|
||||
createdForProviderType: this.value.spec.type,
|
||||
createdForResourceType: 'providers',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
newSecret['_type'] = 'Opaque';
|
||||
newSecret['data'] = {
|
||||
user: btoa(this.newUsername),
|
||||
password: btoa(this.newPassword),
|
||||
insecureSkipVerify: btoa(String(this.skipTlsVerify)),
|
||||
url: btoa(this.value.spec.url),
|
||||
};
|
||||
|
||||
if (this.newCaCert) {
|
||||
newSecret.data.cacert = btoa(this.newCaCert);
|
||||
}
|
||||
|
||||
await newSecret.save();
|
||||
|
||||
this.value.spec.secret = {
|
||||
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="saveProvider"
|
||||
@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.forklift.titles.basic')"
|
||||
:weight="2"
|
||||
>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model:value="value.spec.type"
|
||||
:options="providerTypes"
|
||||
:label="t('harvester.addons.forklift.fields.providerType')"
|
||||
:mode="mode"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="value.spec.type !== 'openshift'"
|
||||
class="col span-6"
|
||||
>
|
||||
<LabeledInput
|
||||
v-model:value="value.spec.url"
|
||||
:label="t('harvester.addons.forklift.fields.url')"
|
||||
:placeholder="t('harvester.addons.forklift.placeholders.url')"
|
||||
:mode="mode"
|
||||
:rules="fvGetAndReportPathRules('spec.url')"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
v-if="value.spec.type !== 'openshift'"
|
||||
name="auth"
|
||||
:label="t('harvester.addons.forklift.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.forklift.fields.username')"
|
||||
:mode="mode"
|
||||
:rules="[usernameRule]"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="newPassword"
|
||||
type="password"
|
||||
:label="t('harvester.addons.forklift.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.forklift.fields.caCert')"
|
||||
:placeholder="t('harvester.addons.forklift.placeholders.caCert')"
|
||||
:min-height="100"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-12">
|
||||
<Checkbox
|
||||
v-model:value="skipTlsVerify"
|
||||
:label="t('harvester.addons.forklift.fields.insecureSkipVerify')"
|
||||
: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.secret.name"
|
||||
:options="secretOptions"
|
||||
:label="t('harvester.addons.forklift.fields.selectSecret')"
|
||||
:mode="mode"
|
||||
:rules="[secretRule]"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</CruResource>
|
||||
</template>
|
||||
@ -1,208 +0,0 @@
|
||||
<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 CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import FormValidation from '@shell/mixins/form-validation';
|
||||
import ForkliftProvider from '../mixins/forklift-provider';
|
||||
import { HCI } from '../types';
|
||||
import { STORAGE_CLASS } from '@shell/config/types';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'EditForkliftStorageMap',
|
||||
|
||||
emits: ['update:value'],
|
||||
|
||||
components: {
|
||||
CruResource,
|
||||
Tabbed,
|
||||
Tab,
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
NameNsDescription,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView, FormValidation, ForkliftProvider],
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
this.allProviders = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
|
||||
|
||||
try {
|
||||
this.allStorageClasses = await this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS });
|
||||
} catch {
|
||||
this.allStorageClasses = [];
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
if (!this.value.spec) {
|
||||
this.value.spec = {
|
||||
map: [{ source: { id: '' }, destination: { storageClass: '' } }],
|
||||
provider: {
|
||||
source: {},
|
||||
destination: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
allStorageClasses: [],
|
||||
|
||||
fvFormRuleSets: [
|
||||
{ path: 'metadata.name', rules: ['nameRequired'] },
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
storageClassOptions() {
|
||||
return this.allStorageClasses.map((sc) => ({
|
||||
label: sc.metadata.name,
|
||||
value: sc.metadata.name,
|
||||
}));
|
||||
},
|
||||
|
||||
fvExtraRules() {
|
||||
return { nameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.fields.name') }) : undefined };
|
||||
},
|
||||
|
||||
isFormValid() {
|
||||
return this.fvFormIsValid &&
|
||||
!!this.value.spec.provider?.source?.name &&
|
||||
!!this.value.spec.provider?.destination?.name;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
addMapping() {
|
||||
this.value.spec.map.push({ source: { id: '' }, destination: { storageClass: '' } });
|
||||
},
|
||||
|
||||
removeMapping(idx) {
|
||||
this.value.spec.map.splice(idx, 1);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CruResource
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:errors="errors"
|
||||
:apply-hooks="applyHooks"
|
||||
:validation-passed="isFormValid"
|
||||
@finish="save"
|
||||
@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="providers"
|
||||
:label="t('harvester.addons.forklift.titles.providers')"
|
||||
:weight="3"
|
||||
>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
:value="value.spec.provider.source.name"
|
||||
:options="providerOptions"
|
||||
:label="t('harvester.addons.forklift.fields.sourceProvider')"
|
||||
:mode="mode"
|
||||
required
|
||||
@update:value="updateSourceProvider"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
:value="value.spec.provider.destination.name"
|
||||
:options="providerOptions"
|
||||
:label="t('harvester.addons.forklift.fields.destProvider')"
|
||||
:mode="mode"
|
||||
required
|
||||
@update:value="updateDestProvider"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="mappings"
|
||||
:label="t('harvester.addons.forklift.titles.storageMappings')"
|
||||
:weight="2"
|
||||
>
|
||||
<div
|
||||
v-for="(entry, idx) in value.spec.map"
|
||||
:key="idx"
|
||||
class="row mb-20"
|
||||
>
|
||||
<div class="col span-5">
|
||||
<LabeledInput
|
||||
v-model:value="entry.source.id"
|
||||
:label="t('harvester.addons.forklift.fields.sourceDatastoreId')"
|
||||
:placeholder="t('harvester.addons.forklift.placeholders.sourceDatastoreId')"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-5">
|
||||
<LabeledSelect
|
||||
v-model:value="entry.destination.storageClass"
|
||||
:options="storageClassOptions"
|
||||
:label="t('harvester.addons.forklift.fields.destStorageClass')"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn role-link"
|
||||
:disabled="value.spec.map.length <= 1"
|
||||
@click="removeMapping(idx)"
|
||||
>
|
||||
{{ t('harvester.addons.forklift.actions.remove') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn role-tertiary"
|
||||
@click="addMapping"
|
||||
>
|
||||
{{ t('harvester.addons.forklift.actions.addMapping') }}
|
||||
</button>
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</CruResource>
|
||||
</template>
|
||||
@ -1809,86 +1809,94 @@ harvester:
|
||||
|
||||
forklift:
|
||||
labels:
|
||||
dashboard: Create
|
||||
provider: Providers
|
||||
networkMap: Network Maps
|
||||
storageMap: Storage Maps
|
||||
plan: Migration Plans
|
||||
migration: Migrations
|
||||
titles:
|
||||
basic: Basic
|
||||
auth: Authentication
|
||||
providers: Providers
|
||||
networkMappings: Network Mappings
|
||||
storageMappings: Storage Mappings
|
||||
mappings: Mappings
|
||||
vms: Virtual Machines
|
||||
fields:
|
||||
providerType: Provider Type
|
||||
providerTypeVsphere: vSphere
|
||||
providerTypeOpenstack: OpenStack
|
||||
providerTypeOpenshift: OpenShift / Harvester (Local)
|
||||
url: URL
|
||||
username: Username
|
||||
password: Password
|
||||
caCert: CA Certificate (PEM)
|
||||
insecureSkipVerify: Skip TLS Certificate Verification
|
||||
selectSecret: Select Secret
|
||||
createSecret: Create New Credentials
|
||||
useSecret: Use Existing Secret
|
||||
sourceProvider: Source Provider
|
||||
destProvider: Destination Provider
|
||||
sourceNetworkId: Source Network ID
|
||||
destNetworkType: Destination Network Type
|
||||
destNetworkPod: Pod Network
|
||||
destNetworkMultus: Multus
|
||||
sourceDatastoreId: Source Datastore ID
|
||||
destStorageClass: Destination Storage Class
|
||||
networkMap: Network Map
|
||||
storageMap: Storage Map
|
||||
configureProvider:
|
||||
title: Configure Provider
|
||||
description: Connect to your VMware vCenter or ESXi host to discover virtual machines for migration
|
||||
connectionDetails: Connection Details
|
||||
requirementsTitle: Requirements
|
||||
requirementsText: "To migrate virtual machines from VMWare, you will need a vCenter Server running version 6.5 or higher with a user account that has at least read permissions, and the source environment must be network-accessible from Rancher."
|
||||
name: Name
|
||||
urlLabel: vCenter/ESXi URL
|
||||
urlPlaceholder: "https://vcenter.example.com/sdk"
|
||||
urlHint: Enter the full URL including https://
|
||||
skipSsl: Skip SSL certificate verification
|
||||
skipSslHint: Not recommended for production environments
|
||||
testConnection: Test Connection
|
||||
testSuccess: Connection test passed
|
||||
testFailed: Connection test failed
|
||||
testMissingFields: Please fill in all required fields before testing
|
||||
testTimeout: Connection test timed out — the provider did not report a status in time
|
||||
save: Save Provider and Continue
|
||||
selectVms:
|
||||
title: Select Virtual Machines
|
||||
discovered: "{count} VMs discovered from"
|
||||
lastSynced: Last synced 5 minutes ago
|
||||
availableVms: Available Virtual Machines
|
||||
selected: selected
|
||||
saveSelection: Save Selection and Continue
|
||||
manualEntry:
|
||||
title: Add Virtual Machines
|
||||
description: No VMs were discovered from the provider inventory. Add VMs manually by entering their IDs and names.
|
||||
vmId: VM ID
|
||||
vmIdPlaceholder: "e.g. vm-60527"
|
||||
add: Add
|
||||
added: VMs added
|
||||
columns:
|
||||
vmName: VM Name
|
||||
vmId: VM ID
|
||||
os: Operating System
|
||||
resources: Resources
|
||||
powerState: Power State
|
||||
network: Network
|
||||
datastore: Datastore
|
||||
dashboard:
|
||||
title: VMware to Harvester Migration
|
||||
description: Migrate virtual machines from VMware vSphere to Harvester using cold migration for data consistency. Create a migration plan to select VMs, map networks and storage, and prepare a migration to Harvester.
|
||||
createPlan: Create Migration Plan
|
||||
tableTitle: Migration Plans
|
||||
columns:
|
||||
status: Status
|
||||
plan: Migration Plan
|
||||
progress: Progress
|
||||
mappings: Mappings
|
||||
configureMappings:
|
||||
title: Set Mappings
|
||||
description: Map VMware networks and datastores to Harvester and Longhorn target resources
|
||||
save: Save Mappings and Continue
|
||||
networkMapping:
|
||||
title: Network Mapping
|
||||
description: Map VMware port groups to Harvester networks
|
||||
placeholder: Choose a Harvester network...
|
||||
storageMapping:
|
||||
title: Storage Mapping
|
||||
description: Map VMware datastores to Harvester storage classes
|
||||
placeholder: Choose a Longhorn Storage Volume
|
||||
reviewMigration:
|
||||
title: Review Migration Plan
|
||||
description: Confirm your migration settings before starting the transfer of VMs to the target cluster.
|
||||
migrationDetails: Migration Details
|
||||
totalVms: Total VMs
|
||||
vcpu: vCPU
|
||||
memory: Memory
|
||||
storage: Storage
|
||||
source: Source
|
||||
targetNamespace: Target Namespace
|
||||
migrateSharedDisks: Migrate Shared Disks
|
||||
vmId: VM ID
|
||||
vmName: VM Name
|
||||
plan: Migration Plan
|
||||
placeholders:
|
||||
url: "e.g. https://vcenter/sdk"
|
||||
caCert: "-----BEGIN CERTIFICATE----- ..."
|
||||
sourceNetworkId: "e.g. dvportgroup-13025"
|
||||
sourceDatastoreId: "e.g. datastore-2040"
|
||||
vmId: "e.g. vm-13007"
|
||||
vmName: "e.g. my-vm"
|
||||
actions:
|
||||
addMapping: Add Mapping
|
||||
addVm: Add VM
|
||||
remove: Remove
|
||||
detail:
|
||||
plan: Plan
|
||||
planNamespace: Plan Namespace
|
||||
started: Started
|
||||
completed: Completed
|
||||
vmId: VM ID
|
||||
vmPhase: Phase
|
||||
vmStarted: Started
|
||||
vmCompleted: Completed
|
||||
vmCount: VMs in Plan
|
||||
overallProgress: Overall Progress
|
||||
pipelineSteps: Pipeline Steps
|
||||
migrationName: Migration
|
||||
migrationRun: "Migration #{idx}"
|
||||
noCurrentMigration: No migration is currently running for this plan.
|
||||
noHistory: No migration history available.
|
||||
status:
|
||||
label: Status
|
||||
succeeded: Succeeded
|
||||
failed: Failed
|
||||
running: Running
|
||||
executing: Executing
|
||||
pending: Pending
|
||||
ready: Ready
|
||||
unknown: Unknown
|
||||
tabs:
|
||||
currentMigration: Current Migration
|
||||
history: Migration History
|
||||
migrationMode: Migration Mode
|
||||
coldMigration: Cold Migration
|
||||
vmsWillShutDown: VMs will shut down
|
||||
virtualMachines: Virtual Machines
|
||||
warningTitle: "Cold Migration Warning"
|
||||
warningMessage: "All source VMs will be powered off, and downtime will occur during migration. Plan your maintenance window accordingly and notify relevant stakeholders"
|
||||
startMigration: Start Migration Plan
|
||||
|
||||
vmImport:
|
||||
titles:
|
||||
|
||||
@ -1,60 +0,0 @@
|
||||
<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.FORKLIFT_MIGRATION,
|
||||
type: SCHEMA,
|
||||
attributes: {
|
||||
kind: HCI.FORKLIFT_MIGRATION,
|
||||
namespaced: true
|
||||
},
|
||||
metadata: { name: HCI.FORKLIFT_MIGRATION },
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'HarvesterForkliftMigration',
|
||||
components: { ResourceTable, Loading },
|
||||
inheritAttrs: false,
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_MIGRATION });
|
||||
|
||||
const configSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.FORKLIFT_MIGRATION);
|
||||
|
||||
if (!configSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) {
|
||||
this.$store.dispatch('type-map/configureType', { match: HCI.FORKLIFT_MIGRATION, 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>
|
||||
@ -1,60 +0,0 @@
|
||||
<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.FORKLIFT_NETWORK_MAP,
|
||||
type: SCHEMA,
|
||||
attributes: {
|
||||
kind: HCI.FORKLIFT_NETWORK_MAP,
|
||||
namespaced: true
|
||||
},
|
||||
metadata: { name: HCI.FORKLIFT_NETWORK_MAP },
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'HarvesterForkliftNetworkMap',
|
||||
components: { ResourceTable, Loading },
|
||||
inheritAttrs: false,
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_NETWORK_MAP });
|
||||
|
||||
const configSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.FORKLIFT_NETWORK_MAP);
|
||||
|
||||
if (!configSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) {
|
||||
this.$store.dispatch('type-map/configureType', { match: HCI.FORKLIFT_NETWORK_MAP, 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>
|
||||
@ -1,60 +0,0 @@
|
||||
<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.FORKLIFT_PLAN,
|
||||
type: SCHEMA,
|
||||
attributes: {
|
||||
kind: HCI.FORKLIFT_PLAN,
|
||||
namespaced: true
|
||||
},
|
||||
metadata: { name: HCI.FORKLIFT_PLAN },
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'HarvesterForkliftPlan',
|
||||
components: { ResourceTable, Loading },
|
||||
inheritAttrs: false,
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PLAN });
|
||||
|
||||
const configSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.FORKLIFT_PLAN);
|
||||
|
||||
if (!configSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) {
|
||||
this.$store.dispatch('type-map/configureType', { match: HCI.FORKLIFT_PLAN, 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>
|
||||
@ -1,60 +0,0 @@
|
||||
<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.FORKLIFT_PROVIDER,
|
||||
type: SCHEMA,
|
||||
attributes: {
|
||||
kind: HCI.FORKLIFT_PROVIDER,
|
||||
namespaced: true
|
||||
},
|
||||
metadata: { name: HCI.FORKLIFT_PROVIDER },
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'HarvesterForkliftProvider',
|
||||
components: { ResourceTable, Loading },
|
||||
inheritAttrs: false,
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
|
||||
|
||||
const configSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.FORKLIFT_PROVIDER);
|
||||
|
||||
if (!configSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) {
|
||||
this.$store.dispatch('type-map/configureType', { match: HCI.FORKLIFT_PROVIDER, 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>
|
||||
@ -1,60 +0,0 @@
|
||||
<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.FORKLIFT_STORAGE_MAP,
|
||||
type: SCHEMA,
|
||||
attributes: {
|
||||
kind: HCI.FORKLIFT_STORAGE_MAP,
|
||||
namespaced: true
|
||||
},
|
||||
metadata: { name: HCI.FORKLIFT_STORAGE_MAP },
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'HarvesterForkliftStorageMap',
|
||||
components: { ResourceTable, Loading },
|
||||
inheritAttrs: false,
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
this.rows = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_STORAGE_MAP });
|
||||
|
||||
const configSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.FORKLIFT_STORAGE_MAP);
|
||||
|
||||
if (!configSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) {
|
||||
this.$store.dispatch('type-map/configureType', { match: HCI.FORKLIFT_STORAGE_MAP, 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>
|
||||
@ -1,47 +0,0 @@
|
||||
import { FORKLIFT_API_VERSION } from '../config/harvester-map';
|
||||
|
||||
/**
|
||||
* Shared mixin for Forklift edit pages that need to select
|
||||
* source and destination providers from available Provider CRs.
|
||||
*
|
||||
* Components using this mixin must load providers in their own fetch():
|
||||
* this.allProviders = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return { allProviders: [] };
|
||||
},
|
||||
|
||||
computed: {
|
||||
providerOptions() {
|
||||
return this.allProviders.map((p) => ({
|
||||
label: `${ p.metadata.name } (${ p.spec?.type || 'unknown' })`,
|
||||
value: p.metadata.name,
|
||||
}));
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Build a Forklift object reference for a Provider.
|
||||
*/
|
||||
buildProviderRef(name) {
|
||||
const provider = this.allProviders.find((p) => p.metadata.name === name);
|
||||
|
||||
return {
|
||||
apiVersion: FORKLIFT_API_VERSION,
|
||||
kind: 'Provider',
|
||||
name,
|
||||
namespace: provider?.metadata?.namespace || this.value.metadata.namespace || 'default',
|
||||
};
|
||||
},
|
||||
|
||||
updateSourceProvider(val) {
|
||||
this.value.spec.provider.source = this.buildProviderRef(val);
|
||||
},
|
||||
|
||||
updateDestProvider(val) {
|
||||
this.value.spec.provider.destination = this.buildProviderRef(val);
|
||||
},
|
||||
},
|
||||
};
|
||||
556
pkg/harvester/pages/c/_cluster/forklift/configure-mappings.vue
Normal file
556
pkg/harvester/pages/c/_cluster/forklift/configure-mappings.vue
Normal file
@ -0,0 +1,556 @@
|
||||
<script>
|
||||
import { defineComponent, ref, computed } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import Masthead from '@shell/components/ResourceList/Masthead';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { RcItemCard } from '@components/RcItemCard';
|
||||
import { SCHEMA, STORAGE_CLASS, NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||
import { useI18n } from '@shell/composables/useI18n';
|
||||
import { HCI } from '../../../../types';
|
||||
import { PRODUCT_NAME } from '../../../../config/harvester';
|
||||
import vmData from '../../../../utils/vms.json';
|
||||
|
||||
const schema = {
|
||||
id: HCI.FORKLIFT_NETWORK_MAP,
|
||||
type: SCHEMA,
|
||||
attributes: {
|
||||
kind: HCI.FORKLIFT_NETWORK_MAP,
|
||||
namespaced: true
|
||||
},
|
||||
metadata: { name: HCI.FORKLIFT_NETWORK_MAP },
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ForkliftConfigureMappings',
|
||||
|
||||
components: {
|
||||
Loading,
|
||||
Masthead,
|
||||
AsyncButton,
|
||||
LabeledSelect,
|
||||
RcItemCard,
|
||||
},
|
||||
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n(store);
|
||||
|
||||
const vms = ref([]);
|
||||
const harvesterNetworks = ref([]);
|
||||
const storageClasses = ref([]);
|
||||
const networkEntries = ref([]);
|
||||
const storageEntries = ref([]);
|
||||
const errors = ref([]);
|
||||
const loading = ref(true);
|
||||
|
||||
const providerName = computed(() => route.query.provider || 'vsphere');
|
||||
const namespace = computed(() => route.query.namespace || 'default');
|
||||
|
||||
const harvesterNetworkOptions = computed(() => {
|
||||
const options = [
|
||||
{ label: 'Pod Networking', value: 'pod' },
|
||||
{ label: 'Ignored', value: 'ignored' },
|
||||
];
|
||||
|
||||
harvesterNetworks.value.forEach((net) => {
|
||||
const ns = net.metadata?.namespace || '';
|
||||
const name = net.metadata?.name || net.id;
|
||||
const fullName = ns ? `${ ns }/${ name }` : name;
|
||||
|
||||
options.push({ label: fullName, value: fullName });
|
||||
});
|
||||
|
||||
return options;
|
||||
});
|
||||
|
||||
const storageClassOptions = computed(() => {
|
||||
const options = storageClasses.value.map((sc) => ({
|
||||
label: sc.metadata?.name || sc.id,
|
||||
value: sc.metadata?.name || sc.id,
|
||||
}));
|
||||
|
||||
if (!options.find((o) => o.value === 'harvester-longhorn')) {
|
||||
options.unshift({ label: 'harvester-longhorn', value: 'harvester-longhorn' });
|
||||
}
|
||||
|
||||
return options;
|
||||
});
|
||||
|
||||
const allNetworksMapped = computed(() => networkEntries.value.length > 0 && networkEntries.value.every((e) => !!e.target));
|
||||
const allStorageMapped = computed(() => storageEntries.value.length > 0 && storageEntries.value.every((e) => !!e.target));
|
||||
|
||||
const canSave = computed(() => {
|
||||
return networkEntries.value.length > 0 && storageEntries.value.length > 0 &&
|
||||
allNetworksMapped.value && allStorageMapped.value;
|
||||
});
|
||||
|
||||
const buildNetworkEntries = () => {
|
||||
const entries = [];
|
||||
|
||||
vms.value.forEach((vm) => {
|
||||
if (vm.networks && vm.networks.length > 0) {
|
||||
vm.networks.forEach((net) => {
|
||||
entries.push({
|
||||
vmId: vm.id,
|
||||
vmName: vm.name || vm.id,
|
||||
name: net.name || 'Unknown',
|
||||
id: net.id || '',
|
||||
vlanId: net.vlanId || '',
|
||||
target: 'pod',
|
||||
_key: `net-${ vm.id }-${ net.id || net.name }`,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
entries.push({
|
||||
vmId: vm.id,
|
||||
vmName: vm.name || vm.id,
|
||||
name: 'Default Network',
|
||||
id: '',
|
||||
vlanId: '',
|
||||
target: 'pod',
|
||||
_key: `net-${ vm.id }-default`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
networkEntries.value = entries;
|
||||
};
|
||||
|
||||
const buildStorageEntries = () => {
|
||||
const entries = [];
|
||||
|
||||
vms.value.forEach((vm) => {
|
||||
if (vm.disks && vm.disks.length > 0) {
|
||||
const seen = new Set();
|
||||
|
||||
vm.disks.forEach((disk) => {
|
||||
const ds = disk.datastore;
|
||||
|
||||
if (ds && ds.id && !seen.has(ds.id)) {
|
||||
seen.add(ds.id);
|
||||
entries.push({
|
||||
vmId: vm.id,
|
||||
vmName: vm.name || vm.id,
|
||||
name: ds.name || 'Unknown',
|
||||
id: ds.id || '',
|
||||
type: ds.type || '',
|
||||
capacity: disk.capacity || 0,
|
||||
target: 'harvester-longhorn',
|
||||
_key: `stor-${ vm.id }-${ ds.id }`,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
entries.push({
|
||||
vmId: vm.id,
|
||||
vmName: vm.name || vm.id,
|
||||
name: 'Default Datastore',
|
||||
id: '',
|
||||
type: '',
|
||||
capacity: 0,
|
||||
target: 'harvester-longhorn',
|
||||
_key: `stor-${ vm.id }-default`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
storageEntries.value = entries;
|
||||
};
|
||||
|
||||
const formatStorageDetail = (entry) => {
|
||||
const parts = [];
|
||||
|
||||
if (entry.type) {
|
||||
parts.push(entry.type);
|
||||
}
|
||||
|
||||
if (entry.capacity) {
|
||||
const gb = entry.capacity / (1024 * 1024 * 1024);
|
||||
|
||||
if (gb >= 1024) {
|
||||
parts.push(`${ (gb / 1024).toFixed(1) } TB`);
|
||||
} else {
|
||||
parts.push(`${ Math.round(gb) } GB`);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join(' \u2022 ');
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
router.push({
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
||||
params: {
|
||||
product: route.params.product,
|
||||
cluster: route.params.cluster,
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const saveMappings = async(buttonCb) => {
|
||||
const inStore = store.getters['currentProduct'].inStore;
|
||||
|
||||
const providerRef = {
|
||||
source: {
|
||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||
kind: 'Provider',
|
||||
name: providerName.value,
|
||||
namespace: namespace.value,
|
||||
},
|
||||
destination: {
|
||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||
kind: 'Provider',
|
||||
name: 'host',
|
||||
namespace: 'forklift',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
// Create NetworkMap
|
||||
const networkMapSpec = {
|
||||
map: networkEntries.value.map((entry) => {
|
||||
if (entry.target === 'pod') {
|
||||
return {
|
||||
source: { name: entry.name, id: entry.id },
|
||||
destination: { type: 'pod' },
|
||||
};
|
||||
}
|
||||
|
||||
if (entry.target === 'ignored') {
|
||||
return {
|
||||
source: { name: entry.name, id: entry.id },
|
||||
destination: { type: 'ignored' },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
source: { name: entry.name, id: entry.id },
|
||||
destination: {
|
||||
type: 'multus',
|
||||
name: entry.target,
|
||||
namespace: namespace.value,
|
||||
},
|
||||
};
|
||||
}),
|
||||
provider: providerRef,
|
||||
};
|
||||
|
||||
const networkMap = await store.dispatch(`${ inStore }/create`, {
|
||||
type: HCI.FORKLIFT_NETWORK_MAP,
|
||||
metadata: {
|
||||
name: `${ providerName.value }-network-map-${ Math.random().toString(36).substring(2, 7) }`,
|
||||
namespace: namespace.value,
|
||||
},
|
||||
spec: networkMapSpec,
|
||||
});
|
||||
|
||||
await networkMap.save();
|
||||
|
||||
// Create StorageMap
|
||||
const storageMapSpec = {
|
||||
map: storageEntries.value.map((entry) => ({
|
||||
source: { name: entry.name, id: entry.id },
|
||||
destination: { storageClass: entry.target },
|
||||
})),
|
||||
provider: providerRef,
|
||||
};
|
||||
|
||||
const storageMap = await store.dispatch(`${ inStore }/create`, {
|
||||
type: HCI.FORKLIFT_STORAGE_MAP,
|
||||
metadata: {
|
||||
name: `${ providerName.value }-storage-map-${ Math.random().toString(36).substring(2, 7) }`,
|
||||
namespace: namespace.value,
|
||||
},
|
||||
spec: storageMapSpec,
|
||||
});
|
||||
|
||||
await storageMap.save();
|
||||
|
||||
// Navigate to review page with all mapping data
|
||||
const networkMappingsData = networkEntries.value.map((entry) => ({
|
||||
vmId: entry.vmId,
|
||||
source: entry.name,
|
||||
target: entry.target === 'pod' ? 'Pod Networking' : entry.target === 'ignored' ? 'Ignored' : entry.target,
|
||||
}));
|
||||
|
||||
const storageMappingsData = storageEntries.value.map((entry) => ({
|
||||
vmId: entry.vmId,
|
||||
source: entry.name,
|
||||
target: entry.target,
|
||||
}));
|
||||
|
||||
router.push({
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-review-migration`,
|
||||
params: {
|
||||
product: route.params.product,
|
||||
cluster: route.params.cluster,
|
||||
},
|
||||
query: {
|
||||
provider: providerName.value,
|
||||
namespace: namespace.value,
|
||||
vms: route.query.vms,
|
||||
networkMap: networkMap.metadata.name,
|
||||
storageMap: storageMap.metadata.name,
|
||||
networkMappings: JSON.stringify(networkMappingsData),
|
||||
storageMappings: JSON.stringify(storageMappingsData),
|
||||
}
|
||||
});
|
||||
|
||||
buttonCb(true);
|
||||
} catch (err) {
|
||||
errors.value = [err.message || err];
|
||||
buttonCb(false);
|
||||
}
|
||||
};
|
||||
|
||||
const init = async() => {
|
||||
const inStore = store.getters['currentProduct'].inStore;
|
||||
|
||||
try {
|
||||
harvesterNetworks.value = await store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT });
|
||||
} catch (e) {
|
||||
harvesterNetworks.value = [];
|
||||
}
|
||||
|
||||
try {
|
||||
storageClasses.value = await store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS });
|
||||
} catch (e) {
|
||||
storageClasses.value = [];
|
||||
}
|
||||
|
||||
const vmsParam = route.query.vms;
|
||||
|
||||
if (vmsParam) {
|
||||
try {
|
||||
const vmIds = JSON.parse(vmsParam);
|
||||
|
||||
vms.value = vmIds.map((id) => {
|
||||
const found = vmData.find((vm) => vm.id === id);
|
||||
|
||||
return found || {
|
||||
id, name: id, networks: [], disks: []
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
vms.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
buildNetworkEntries();
|
||||
buildStorageEntries();
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
return {
|
||||
schema,
|
||||
loading,
|
||||
networkEntries,
|
||||
storageEntries,
|
||||
errors,
|
||||
harvesterNetworkOptions,
|
||||
storageClassOptions,
|
||||
canSave,
|
||||
t,
|
||||
formatStorageDetail,
|
||||
cancel,
|
||||
saveMappings,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="loading" />
|
||||
<div
|
||||
v-else
|
||||
class="configure-mappings"
|
||||
>
|
||||
<Masthead
|
||||
:schema="schema"
|
||||
:resource="schema.id"
|
||||
:type-display="t('harvester.addons.forklift.configureMappings.title')"
|
||||
:is-creatable="false"
|
||||
>
|
||||
<template #subHeader>
|
||||
<p class="text-muted line-height-20 mmt-5">
|
||||
{{ t('harvester.addons.forklift.configureMappings.description') }}
|
||||
</p>
|
||||
</template>
|
||||
</Masthead>
|
||||
|
||||
<div class="mappings-columns">
|
||||
<!-- Network Mapping -->
|
||||
<div class="mapping-column">
|
||||
<div>
|
||||
<h3 class="mapping-section-title">
|
||||
{{ t('harvester.addons.forklift.configureMappings.networkMapping.title') }}
|
||||
</h3>
|
||||
<p class="text-muted line-height-20">
|
||||
{{ t('harvester.addons.forklift.configureMappings.networkMapping.description') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<RcItemCard
|
||||
v-for="entry in networkEntries"
|
||||
:id="entry._key"
|
||||
:key="entry._key"
|
||||
:variant="'small'"
|
||||
:header="{ title: { text: entry.vmId }, statuses: entry.target ? [{ icon: 'icon-notify-tick', color: 'text-success' }] : [] }"
|
||||
>
|
||||
<template #item-card-content>
|
||||
<div class="card-content-row">
|
||||
<div class="mapping-source">
|
||||
<span class="source-name">{{ entry.name }}</span>
|
||||
<span class="source-id text-muted">{{ entry.id }}</span>
|
||||
<span class="source-detail text-muted">VLAN {{ entry.vlanId || '0' }}</span>
|
||||
</div>
|
||||
<div class="mapping-arrow">
|
||||
→
|
||||
</div>
|
||||
<div class="mapping-target">
|
||||
<LabeledSelect
|
||||
v-model:value="entry.target"
|
||||
:options="harvesterNetworkOptions"
|
||||
:placeholder="t('harvester.addons.forklift.configureMappings.networkMapping.placeholder')"
|
||||
:searchable="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</RcItemCard>
|
||||
</div>
|
||||
|
||||
<!-- Storage Mapping -->
|
||||
<div class="mapping-column">
|
||||
<div>
|
||||
<h3 class="mapping-section-title">
|
||||
{{ t('harvester.addons.forklift.configureMappings.storageMapping.title') }}
|
||||
</h3>
|
||||
<p class="text-muted">
|
||||
{{ t('harvester.addons.forklift.configureMappings.storageMapping.description') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<RcItemCard
|
||||
v-for="entry in storageEntries"
|
||||
:id="entry._key"
|
||||
:key="entry._key"
|
||||
:variant="'small'"
|
||||
:header="{ title: { text: entry.vmId }, statuses: entry.target ? [{ icon: 'icon-notify-tick', color: 'text-success' }] : [] }"
|
||||
>
|
||||
<template #item-card-content>
|
||||
<div class="card-content-row">
|
||||
<div class="mapping-source">
|
||||
<span class="source-name">{{ entry.name }}</span>
|
||||
<span class="source-id text-muted">{{ entry.id }}</span>
|
||||
<span
|
||||
v-if="entry.type || entry.capacity"
|
||||
class="source-detail text-muted"
|
||||
>{{ formatStorageDetail(entry) }}</span>
|
||||
</div>
|
||||
<div class="mapping-arrow">
|
||||
→
|
||||
</div>
|
||||
<div class="mapping-target">
|
||||
<LabeledSelect
|
||||
v-model:value="entry.target"
|
||||
:options="storageClassOptions"
|
||||
:placeholder="t('harvester.addons.forklift.configureMappings.storageMapping.placeholder')"
|
||||
:searchable="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</RcItemCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions-footer">
|
||||
<button
|
||||
class="btn role-secondary"
|
||||
@click="cancel"
|
||||
>
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
<AsyncButton
|
||||
:disabled="!canSave"
|
||||
:action-label="t('harvester.addons.forklift.configureMappings.save')"
|
||||
:waiting-label="t('harvester.addons.forklift.configureMappings.save')"
|
||||
:success-label="t('harvester.addons.forklift.configureMappings.save')"
|
||||
:error-label="t('harvester.addons.forklift.configureMappings.save')"
|
||||
@click="saveMappings"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.configure-mappings {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.mappings-columns {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 64px;
|
||||
}
|
||||
|
||||
.mapping-column {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mapping-section-title {
|
||||
font-weight: 600;
|
||||
margin: 0 0 5px 0;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.card-content-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 40px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mapping-source {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 160px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
|
||||
.source-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.mapping-arrow {
|
||||
color: var(--muted);
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mapping-target {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.actions-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.line-height-20 {
|
||||
line-height: 20px;
|
||||
}
|
||||
</style>
|
||||
477
pkg/harvester/pages/c/_cluster/forklift/configure-provider.vue
Normal file
477
pkg/harvester/pages/c/_cluster/forklift/configure-provider.vue
Normal file
@ -0,0 +1,477 @@
|
||||
<script>
|
||||
import { defineComponent, ref, computed } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import { Banner } from '@components/Banner';
|
||||
import Masthead from '@shell/components/ResourceList/Masthead';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { SCHEMA, SECRET } from '@shell/config/types';
|
||||
import { randomStr } from '@shell/utils/string';
|
||||
import { useI18n } from '@shell/composables/useI18n';
|
||||
import { HCI } from '../../../../types';
|
||||
import { PRODUCT_NAME } from '../../../../config/harvester';
|
||||
|
||||
const schema = {
|
||||
id: HCI.FORKLIFT_PROVIDER,
|
||||
type: SCHEMA,
|
||||
attributes: {
|
||||
kind: HCI.FORKLIFT_PROVIDER,
|
||||
namespaced: true
|
||||
},
|
||||
metadata: { name: HCI.FORKLIFT_PROVIDER },
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ForkliftConfigureProvider',
|
||||
|
||||
components: {
|
||||
LabeledInput,
|
||||
Checkbox,
|
||||
Banner,
|
||||
Masthead,
|
||||
AsyncButton,
|
||||
},
|
||||
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n(store);
|
||||
|
||||
const allSecrets = ref([]);
|
||||
const providerName = ref('');
|
||||
const url = ref('');
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const skipTlsVerify = ref(false);
|
||||
const testResult = ref(null);
|
||||
const testError = ref(null);
|
||||
const errors = ref([]);
|
||||
const createdProvider = ref(null);
|
||||
const createdSecret = ref(null);
|
||||
const loading = ref(true);
|
||||
|
||||
const isFormValid = computed(() => !!providerName.value && !!url.value && !!username.value && !!password.value);
|
||||
|
||||
const cancel = () => {
|
||||
router.push({
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
||||
params: {
|
||||
product: route.params.product,
|
||||
cluster: route.params.cluster,
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const testConnection = async(buttonCb) => {
|
||||
testResult.value = null;
|
||||
testError.value = null;
|
||||
|
||||
if (!providerName.value || !url.value || !username.value || !password.value) {
|
||||
testError.value = t('harvester.addons.forklift.configureProvider.testMissingFields');
|
||||
buttonCb(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const inStore = store.getters['currentProduct'].inStore;
|
||||
|
||||
try {
|
||||
if (createdProvider.value) {
|
||||
await createdProvider.value.remove();
|
||||
createdProvider.value = null;
|
||||
}
|
||||
if (createdSecret.value) {
|
||||
await createdSecret.value.remove();
|
||||
createdSecret.value = null;
|
||||
}
|
||||
|
||||
const namespace = 'default';
|
||||
const secretName = `${ providerName.value }-creds-${ randomStr(4).toLowerCase() }`;
|
||||
|
||||
const newSecret = await store.dispatch(`${ inStore }/create`, {
|
||||
type: SECRET,
|
||||
metadata: {
|
||||
name: secretName,
|
||||
namespace,
|
||||
labels: {
|
||||
createdForProviderType: 'vsphere',
|
||||
createdForResourceType: 'providers',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
newSecret['_type'] = 'Opaque';
|
||||
newSecret['data'] = {
|
||||
user: btoa(username.value),
|
||||
password: btoa(password.value),
|
||||
insecureSkipVerify: btoa(String(skipTlsVerify.value)),
|
||||
url: btoa(url.value),
|
||||
};
|
||||
|
||||
await newSecret.save();
|
||||
createdSecret.value = newSecret;
|
||||
|
||||
const provider = await store.dispatch(`${ inStore }/create`, {
|
||||
type: HCI.FORKLIFT_PROVIDER,
|
||||
metadata: {
|
||||
name: providerName.value,
|
||||
namespace,
|
||||
},
|
||||
spec: {
|
||||
type: 'vsphere',
|
||||
url: url.value,
|
||||
secret: {
|
||||
name: secretName,
|
||||
namespace,
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
await provider.save();
|
||||
createdProvider.value = provider;
|
||||
|
||||
const maxAttempts = 15;
|
||||
let attempts = 0;
|
||||
let connected = false;
|
||||
let errorMsg = '';
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
attempts++;
|
||||
|
||||
const refreshed = await store.dispatch(`${ inStore }/find`, {
|
||||
type: HCI.FORKLIFT_PROVIDER,
|
||||
id: `${ namespace }/${ providerName.value }`,
|
||||
opt: { force: true }
|
||||
});
|
||||
|
||||
const conditions = refreshed?.status?.conditions || [];
|
||||
const readyCondition = conditions.find((c) => c.type === 'Ready');
|
||||
const connectionCondition = conditions.find((c) => c.type === 'ConnectionTestSucceeded');
|
||||
|
||||
if (connectionCondition) {
|
||||
if (connectionCondition.status === 'True') {
|
||||
connected = true;
|
||||
break;
|
||||
} else {
|
||||
errorMsg = connectionCondition.message || 'Connection failed';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (readyCondition) {
|
||||
if (readyCondition.status === 'True') {
|
||||
connected = true;
|
||||
break;
|
||||
} else if (readyCondition.status === 'False') {
|
||||
errorMsg = readyCondition.message || 'Provider not ready';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (connected) {
|
||||
testResult.value = t('harvester.addons.forklift.configureProvider.testSuccess');
|
||||
buttonCb(true);
|
||||
} else {
|
||||
if (createdProvider.value) {
|
||||
await createdProvider.value.remove();
|
||||
createdProvider.value = null;
|
||||
}
|
||||
if (createdSecret.value) {
|
||||
await createdSecret.value.remove();
|
||||
createdSecret.value = null;
|
||||
}
|
||||
|
||||
testError.value = errorMsg || t('harvester.addons.forklift.configureProvider.testTimeout');
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
if (createdProvider.value) {
|
||||
try {
|
||||
await createdProvider.value.remove();
|
||||
} catch (e) {}
|
||||
createdProvider.value = null;
|
||||
}
|
||||
if (createdSecret.value) {
|
||||
try {
|
||||
await createdSecret.value.remove();
|
||||
} catch (e) {}
|
||||
createdSecret.value = null;
|
||||
}
|
||||
|
||||
testError.value = err.message || t('harvester.addons.forklift.configureProvider.testFailed');
|
||||
buttonCb(false);
|
||||
}
|
||||
};
|
||||
|
||||
const saveProvider = async(buttonCb) => {
|
||||
const inStore = store.getters['currentProduct'].inStore;
|
||||
|
||||
errors.value = [];
|
||||
|
||||
try {
|
||||
if (createdProvider.value) {
|
||||
router.push({
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-select-vms`,
|
||||
params: {
|
||||
product: route.params.product,
|
||||
cluster: route.params.cluster,
|
||||
},
|
||||
query: {
|
||||
provider: providerName.value,
|
||||
namespace: createdProvider.value.metadata.namespace,
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const namespace = 'default';
|
||||
const secretName = `${ providerName.value }-creds-${ randomStr(4).toLowerCase() }`;
|
||||
|
||||
const newSecret = await store.dispatch(`${ inStore }/create`, {
|
||||
type: SECRET,
|
||||
metadata: {
|
||||
name: secretName,
|
||||
namespace,
|
||||
labels: {
|
||||
createdForProviderType: 'vsphere',
|
||||
createdForResourceType: 'providers',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
newSecret['_type'] = 'Opaque';
|
||||
newSecret['data'] = {
|
||||
user: btoa(username.value),
|
||||
password: btoa(password.value),
|
||||
insecureSkipVerify: btoa(String(skipTlsVerify.value)),
|
||||
url: btoa(url.value),
|
||||
};
|
||||
|
||||
await newSecret.save();
|
||||
|
||||
const provider = await store.dispatch(`${ inStore }/create`, {
|
||||
type: HCI.FORKLIFT_PROVIDER,
|
||||
metadata: {
|
||||
name: providerName.value,
|
||||
namespace,
|
||||
},
|
||||
spec: {
|
||||
type: 'vsphere',
|
||||
url: url.value,
|
||||
secret: {
|
||||
name: secretName,
|
||||
namespace,
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
await provider.save();
|
||||
|
||||
router.push({
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-select-vms`,
|
||||
params: {
|
||||
product: route.params.product,
|
||||
cluster: route.params.cluster,
|
||||
},
|
||||
query: {
|
||||
provider: providerName.value,
|
||||
namespace,
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
errors.value = [err.message || err];
|
||||
buttonCb(false);
|
||||
}
|
||||
};
|
||||
|
||||
const init = async() => {
|
||||
const inStore = store.getters['currentProduct'].inStore;
|
||||
|
||||
allSecrets.value = await store.dispatch(`${ inStore }/findAll`, { type: SECRET });
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
return {
|
||||
schema,
|
||||
allSecrets,
|
||||
providerName,
|
||||
url,
|
||||
username,
|
||||
password,
|
||||
skipTlsVerify,
|
||||
testResult,
|
||||
testError,
|
||||
errors,
|
||||
loading,
|
||||
isFormValid,
|
||||
t,
|
||||
cancel,
|
||||
testConnection,
|
||||
saveProvider,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="configure-provider">
|
||||
<Masthead
|
||||
:schema="schema"
|
||||
:resource="schema.id"
|
||||
:type-display="t('harvester.addons.forklift.configureProvider.title')"
|
||||
:is-creatable="false"
|
||||
>
|
||||
<template #subHeader>
|
||||
<div class="mmt-5">
|
||||
<p class="text-muted">
|
||||
{{ t('harvester.addons.forklift.configureProvider.description') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</Masthead>
|
||||
|
||||
<h3 class="mmt-3 mmb-3 table-title">
|
||||
<b>{{ t('harvester.addons.forklift.configureProvider.connectionDetails') }}</b>
|
||||
</h3>
|
||||
|
||||
<Banner
|
||||
class="requirements-banner mt-0 mmb-3"
|
||||
color="info"
|
||||
>
|
||||
<div class="requirements-banner-content">
|
||||
<span class="requirements-banner-title">{{ t('harvester.addons.forklift.configureProvider.requirementsTitle') }}</span>
|
||||
<br>
|
||||
<span>{{ t('harvester.addons.forklift.configureProvider.requirementsText') }}</span>
|
||||
</div>
|
||||
</Banner>
|
||||
|
||||
<div class="mb-20">
|
||||
<LabeledInput
|
||||
v-model:value="providerName"
|
||||
:label="t('harvester.addons.forklift.configureProvider.name')"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-20">
|
||||
<LabeledInput
|
||||
v-model:value="url"
|
||||
:label="t('harvester.addons.forklift.configureProvider.urlLabel')"
|
||||
:placeholder="t('harvester.addons.forklift.configureProvider.urlPlaceholder')"
|
||||
required
|
||||
/>
|
||||
<p class="text-muted mt-5">
|
||||
{{ t('harvester.addons.forklift.configureProvider.urlHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="username"
|
||||
:label="t('harvester.addons.forklift.fields.username')"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="password"
|
||||
type="password"
|
||||
:label="t('harvester.addons.forklift.fields.password')"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-20">
|
||||
<Checkbox
|
||||
v-model:value="skipTlsVerify"
|
||||
:label="t('harvester.addons.forklift.configureProvider.skipSsl')"
|
||||
/>
|
||||
<p class="text-muted ml-20">
|
||||
{{ t('harvester.addons.forklift.configureProvider.skipSslHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-20">
|
||||
<AsyncButton
|
||||
mode="test"
|
||||
:action-label="t('harvester.addons.forklift.configureProvider.testConnection')"
|
||||
:waiting-label="t('harvester.addons.forklift.configureProvider.testConnection')"
|
||||
:success-label="t('harvester.addons.forklift.configureProvider.testConnection')"
|
||||
:error-label="t('harvester.addons.forklift.configureProvider.testConnection')"
|
||||
class="btn role-secondary"
|
||||
@click="testConnection"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner
|
||||
v-if="testResult"
|
||||
color="success"
|
||||
class="mb-10"
|
||||
>
|
||||
{{ testResult }}
|
||||
</Banner>
|
||||
<Banner
|
||||
v-if="testError"
|
||||
color="error"
|
||||
class="mb-10"
|
||||
>
|
||||
{{ testError }}
|
||||
</Banner>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
class="mb-10"
|
||||
>
|
||||
{{ err }}
|
||||
</Banner>
|
||||
|
||||
<div class="provider-actions">
|
||||
<button
|
||||
class="btn role-secondary"
|
||||
@click="cancel"
|
||||
>
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
<AsyncButton
|
||||
:disabled="!isFormValid"
|
||||
:action-label="t('harvester.addons.forklift.configureProvider.save')"
|
||||
:waiting-label="t('harvester.addons.forklift.configureProvider.save')"
|
||||
:success-label="t('harvester.addons.forklift.configureProvider.save')"
|
||||
:error-label="t('harvester.addons.forklift.configureProvider.save')"
|
||||
@click="saveProvider"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.configure-provider {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.requirements-banner {
|
||||
font-weight: 400;
|
||||
|
||||
.requirements-banner-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.provider-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
</style>
|
||||
342
pkg/harvester/pages/c/_cluster/forklift/index.vue
Normal file
342
pkg/harvester/pages/c/_cluster/forklift/index.vue
Normal file
@ -0,0 +1,342 @@
|
||||
<script>
|
||||
import { defineComponent, ref, computed } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useRoute } from 'vue-router';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import Masthead from '@shell/components/ResourceList/Masthead';
|
||||
import ResourceTable from '@shell/components/ResourceTable';
|
||||
import PercentageBar from '@shell/components/PercentageBar';
|
||||
import MappingsCell from '../../../../components/MappingsCell';
|
||||
import { SCHEMA } from '@shell/config/types';
|
||||
import { useI18n } from '@shell/composables/useI18n';
|
||||
import { HCI } from '../../../../types';
|
||||
import { PRODUCT_NAME } from '../../../../config/harvester';
|
||||
import { FORKLIFT_PLAN_VM_COUNT } from '../../../../config/table-headers';
|
||||
import { STATE, NAME as NAME_COL, AGE } from '@shell/config/table-headers';
|
||||
|
||||
const schema = {
|
||||
id: HCI.FORKLIFT_PLAN,
|
||||
type: SCHEMA,
|
||||
attributes: {
|
||||
kind: HCI.FORKLIFT_PLAN,
|
||||
namespaced: true
|
||||
},
|
||||
metadata: { name: HCI.FORKLIFT_PLAN },
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ForkliftMigrationDashboard',
|
||||
|
||||
components: {
|
||||
Loading,
|
||||
Masthead,
|
||||
MappingsCell,
|
||||
PercentageBar,
|
||||
ResourceTable,
|
||||
},
|
||||
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
const { t } = useI18n(store);
|
||||
|
||||
const loading = ref(true);
|
||||
|
||||
const inStore = computed(() => store.getters['currentProduct'].inStore);
|
||||
|
||||
const allPlans = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_PLAN));
|
||||
const allNetworkMaps = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_NETWORK_MAP));
|
||||
const allStorageMaps = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_STORAGE_MAP));
|
||||
|
||||
const rows = computed(() => {
|
||||
return allPlans.value.map((plan) => {
|
||||
const netMapName = plan.spec?.map?.network?.name;
|
||||
const netMapNs = plan.spec?.map?.network?.namespace || plan.metadata.namespace;
|
||||
const storMapName = plan.spec?.map?.storage?.name;
|
||||
const storMapNs = plan.spec?.map?.storage?.namespace || plan.metadata.namespace;
|
||||
|
||||
const netMap = allNetworkMaps.value.find((m) => m.metadata.name === netMapName && m.metadata.namespace === netMapNs);
|
||||
const storMap = allStorageMaps.value.find((m) => m.metadata.name === storMapName && m.metadata.namespace === storMapNs);
|
||||
|
||||
plan.networkEntries = (netMap?.spec?.map || []).map((e) => `${ e.source?.id || '-' } → ${ e.destination?.type === 'pod' ? 'Pod Network' : (e.destination?.name || '-') }`);
|
||||
plan.storageEntries = (storMap?.spec?.map || []).map((e) => `${ e.source?.id || '-' } → ${ e.destination?.storageClass || '-' }`);
|
||||
plan.networkDisplay = plan.networkEntries.join(', ') || '-';
|
||||
plan.storageDisplay = plan.storageEntries.join(', ') || '-';
|
||||
|
||||
plan.vmIdsDisplay = (plan.spec?.vms || []).map((vm) => vm.id || vm.name || '').filter(Boolean).join(', ') || '-';
|
||||
|
||||
const vms = plan.status?.migration?.vms || [];
|
||||
|
||||
plan.vmProgress = vms.map((vm) => {
|
||||
const pipeline = vm.pipeline || [];
|
||||
const totalSteps = pipeline.length;
|
||||
const completedSteps = pipeline.filter((s) => s.phase === 'Completed').length;
|
||||
|
||||
return {
|
||||
name: vm.id || vm.name || 'Unknown',
|
||||
steps: pipeline.map((step) => ({
|
||||
name: step.name || 'Unknown',
|
||||
progress: step.phase === 'Completed' ? 100 : (step.progress?.completed && step.progress?.total ? Math.round((step.progress.completed / step.progress.total) * 100) : 0),
|
||||
hasError: !!step.error,
|
||||
reason: (step.error?.reasons || []).join('; ') || '',
|
||||
})),
|
||||
progress: totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0,
|
||||
};
|
||||
});
|
||||
|
||||
let totalSteps = 0;
|
||||
let completedSteps = 0;
|
||||
|
||||
vms.forEach((vm) => {
|
||||
const pipeline = vm.pipeline || [];
|
||||
|
||||
totalSteps += pipeline.length;
|
||||
completedSteps += pipeline.filter((s) => s.phase === 'Completed').length;
|
||||
});
|
||||
|
||||
plan.progress = totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0;
|
||||
|
||||
return plan;
|
||||
});
|
||||
});
|
||||
|
||||
const createLocation = computed(() => ({
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-configure-provider`,
|
||||
params: {
|
||||
product: route.params.product,
|
||||
cluster: route.params.cluster,
|
||||
}
|
||||
}));
|
||||
|
||||
const headers = [
|
||||
{ ...STATE, labelKey: 'harvester.addons.forklift.dashboard.columns.status' },
|
||||
{
|
||||
...NAME_COL,
|
||||
labelKey: 'harvester.addons.forklift.dashboard.columns.plan',
|
||||
subLabel: 'VM IDs',
|
||||
},
|
||||
{ ...FORKLIFT_PLAN_VM_COUNT, width: 105 },
|
||||
{
|
||||
name: 'progress',
|
||||
labelKey: 'harvester.addons.forklift.dashboard.columns.progress',
|
||||
value: 'progress',
|
||||
width: 299,
|
||||
},
|
||||
{
|
||||
name: 'mappings',
|
||||
labelKey: 'harvester.addons.forklift.dashboard.columns.mappings',
|
||||
value: 'mappingsDisplay',
|
||||
width: 387,
|
||||
},
|
||||
{ ...AGE },
|
||||
];
|
||||
|
||||
const init = async() => {
|
||||
await Promise.all([
|
||||
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_PLAN }),
|
||||
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_NETWORK_MAP }),
|
||||
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_STORAGE_MAP }),
|
||||
]);
|
||||
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
return {
|
||||
schema,
|
||||
loading,
|
||||
rows,
|
||||
createLocation,
|
||||
headers,
|
||||
t,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="loading" />
|
||||
<div v-else>
|
||||
<Masthead
|
||||
:schema="schema"
|
||||
:resource="schema.id"
|
||||
:type-display="t('harvester.addons.forklift.dashboard.title')"
|
||||
>
|
||||
<template #subHeader>
|
||||
<div class="mmt-5">
|
||||
<p class="text-muted">
|
||||
{{ t('harvester.addons.forklift.dashboard.description') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #createButton>
|
||||
<router-link
|
||||
:to="createLocation"
|
||||
class="btn role-primary"
|
||||
>
|
||||
{{ t('harvester.addons.forklift.dashboard.createPlan') }}
|
||||
</router-link>
|
||||
</template>
|
||||
</Masthead>
|
||||
|
||||
<ResourceTable
|
||||
:schema="schema"
|
||||
:rows="rows"
|
||||
:headers="headers"
|
||||
:groupable="false"
|
||||
:table-actions="false"
|
||||
:search="false"
|
||||
key-field="_key"
|
||||
>
|
||||
<template #header-left>
|
||||
<h3 class="table-title m-0">
|
||||
{{ t('harvester.addons.forklift.dashboard.tableTitle') }}
|
||||
</h3>
|
||||
</template>
|
||||
<template #cell:name="{ row }">
|
||||
<div class="plan-name-cell">
|
||||
<div class="plan-name">
|
||||
{{ row.metadata.name }}
|
||||
</div>
|
||||
<div class="plan-vms text-muted">
|
||||
{{ row.vmIdsDisplay }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell:vmCount="{ row }">
|
||||
{{ (row.spec.vms || []).length }} VMs
|
||||
</template>
|
||||
<template #cell:progress="{ row }">
|
||||
<div
|
||||
v-if="row.vmProgress && row.vmProgress.length"
|
||||
class="progress-steps"
|
||||
>
|
||||
<div
|
||||
v-for="vm in row.vmProgress"
|
||||
:key="vm.name"
|
||||
class="vm-progress"
|
||||
>
|
||||
<div class="vm-name">
|
||||
{{ vm.name }}
|
||||
</div>
|
||||
<div
|
||||
v-for="step in vm.steps"
|
||||
:key="step.name"
|
||||
class="progress-step"
|
||||
>
|
||||
<span
|
||||
class="step-name"
|
||||
:class="{ 'text-error': step.hasError }"
|
||||
>
|
||||
{{ step.name }}
|
||||
</span>
|
||||
<PercentageBar
|
||||
:model-value="step.progress"
|
||||
:color-stops="step.hasError ? { 0: '--error' } : { 99: '--primary', 100: '--success' }"
|
||||
preferred-direction="MORE"
|
||||
class="step-bar"
|
||||
/>
|
||||
<span class="step-pct">{{ step.progress }}%</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="step in vm.steps.filter(s => s.hasError)"
|
||||
:key="`error-${step.name}`"
|
||||
class="step-error-msg"
|
||||
>
|
||||
<span class="text-error">{{ step.reason }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template #cell:mappings="{ row }">
|
||||
<MappingsCell
|
||||
:network-entries="row.networkEntries"
|
||||
:storage-entries="row.storageEntries"
|
||||
/>
|
||||
</template>
|
||||
</ResourceTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.plan-name-cell {
|
||||
.plan-name {
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.plan-vms {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: var(--muted);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vm-progress {
|
||||
&:not(:last-child) {
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
}
|
||||
|
||||
.vm-name {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.progress-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 4px;
|
||||
padding-right: 13px;
|
||||
}
|
||||
|
||||
.step-error-msg {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
|
||||
.text-error {
|
||||
color: var(--error);
|
||||
}
|
||||
}
|
||||
|
||||
.step-name {
|
||||
font-size: 12px;
|
||||
width: 110px;
|
||||
min-width: 110px;
|
||||
max-width: 110px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.text-error {
|
||||
color: var(--error);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.step-bar {
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.step-pct {
|
||||
font-size: 12px;
|
||||
min-width: 35px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
544
pkg/harvester/pages/c/_cluster/forklift/review-migration.vue
Normal file
544
pkg/harvester/pages/c/_cluster/forklift/review-migration.vue
Normal file
@ -0,0 +1,544 @@
|
||||
<script>
|
||||
import { defineComponent, ref, computed } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import Masthead from '@shell/components/ResourceList/Masthead';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { RcItemCard } from '@components/RcItemCard';
|
||||
import MappingsCell from '../../../../components/MappingsCell';
|
||||
import { SCHEMA } from '@shell/config/types';
|
||||
import { useI18n } from '@shell/composables/useI18n';
|
||||
import { HCI } from '../../../../types';
|
||||
import { PRODUCT_NAME } from '../../../../config/harvester';
|
||||
import vmData from '../../../../utils/vms.json';
|
||||
|
||||
const schema = {
|
||||
id: HCI.FORKLIFT_PLAN,
|
||||
type: SCHEMA,
|
||||
attributes: {
|
||||
kind: HCI.FORKLIFT_PLAN,
|
||||
namespaced: true
|
||||
},
|
||||
metadata: { name: HCI.FORKLIFT_PLAN },
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ForkliftReviewMigration',
|
||||
|
||||
components: {
|
||||
Loading,
|
||||
Masthead,
|
||||
AsyncButton,
|
||||
Banner,
|
||||
MappingsCell,
|
||||
RcItemCard,
|
||||
},
|
||||
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n(store);
|
||||
|
||||
const vms = ref([]);
|
||||
const networkMappings = ref([]);
|
||||
const storageMappings = ref([]);
|
||||
const errors = ref([]);
|
||||
const loading = ref(true);
|
||||
|
||||
const providerName = computed(() => route.query.provider || 'vsphere');
|
||||
const namespace = computed(() => route.query.namespace || 'default');
|
||||
const networkMapName = computed(() => route.query.networkMap || '');
|
||||
const storageMapName = computed(() => route.query.storageMap || '');
|
||||
|
||||
const totalVCpu = computed(() => vms.value.reduce((sum, vm) => sum + (vm.cpuCount || vm.numCPU || 0), 0));
|
||||
|
||||
const totalMemoryGB = computed(() => {
|
||||
const totalMB = vms.value.reduce((sum, vm) => sum + (vm.memoryMB || vm.memory || 0), 0);
|
||||
|
||||
return Math.round(totalMB / 1024);
|
||||
});
|
||||
|
||||
const totalStorageGB = computed(() => {
|
||||
let totalBytes = 0;
|
||||
|
||||
vms.value.forEach((vm) => {
|
||||
if (vm.disks && vm.disks.length > 0) {
|
||||
totalBytes += vm.disks.reduce((sum, d) => sum + (d.capacity || 0), 0);
|
||||
}
|
||||
});
|
||||
|
||||
return Math.round(totalBytes / (1024 * 1024 * 1024));
|
||||
});
|
||||
|
||||
const vmCards = computed(() => {
|
||||
return vms.value.map((vm) => {
|
||||
const cpus = vm.cpuCount || vm.numCPU || 0;
|
||||
const memMB = vm.memoryMB || vm.memory || 0;
|
||||
const memGB = memMB ? `${ Math.round(memMB / 1024) } GB` : '-';
|
||||
|
||||
let totalDiskBytes = 0;
|
||||
|
||||
if (vm.disks && vm.disks.length > 0) {
|
||||
totalDiskBytes = vm.disks.reduce((sum, d) => sum + (d.capacity || 0), 0);
|
||||
}
|
||||
|
||||
const diskDisplay = totalDiskBytes ? `${ Math.round(totalDiskBytes / (1024 * 1024 * 1024)) } GB` : '-';
|
||||
const os = vm.guestName || vm.guestOS || vm.os || '-';
|
||||
|
||||
const vmNetworkMappings = networkMappings.value.filter((m) => m.vmId === vm.id);
|
||||
const vmStorageMappings = storageMappings.value.filter((m) => m.vmId === vm.id);
|
||||
|
||||
return {
|
||||
id: vm.id,
|
||||
name: vm.name || vm.id,
|
||||
os,
|
||||
cpus,
|
||||
memGB,
|
||||
diskDisplay,
|
||||
networkMappings: vmNetworkMappings,
|
||||
storageMappings: vmStorageMappings,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const cancel = () => {
|
||||
router.push({
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
||||
params: {
|
||||
product: route.params.product,
|
||||
cluster: route.params.cluster,
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const startMigration = async(buttonCb) => {
|
||||
const inStore = store.getters['currentProduct'].inStore;
|
||||
|
||||
try {
|
||||
const planName = `${ providerName.value }-plan-${ Math.random().toString(36).substring(2, 7) }`;
|
||||
|
||||
const planSpec = {
|
||||
provider: {
|
||||
source: {
|
||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||
kind: 'Provider',
|
||||
name: providerName.value,
|
||||
namespace: namespace.value,
|
||||
},
|
||||
destination: {
|
||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||
kind: 'Provider',
|
||||
name: 'host',
|
||||
namespace: 'forklift',
|
||||
},
|
||||
},
|
||||
map: {
|
||||
network: {
|
||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||
kind: 'NetworkMap',
|
||||
name: networkMapName.value,
|
||||
namespace: namespace.value,
|
||||
},
|
||||
storage: {
|
||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||
kind: 'StorageMap',
|
||||
name: storageMapName.value,
|
||||
namespace: namespace.value,
|
||||
},
|
||||
},
|
||||
targetNamespace: namespace.value,
|
||||
vms: vms.value.map((vm) => ({
|
||||
id: vm.id,
|
||||
name: vm.name || vm.id,
|
||||
})),
|
||||
warm: false,
|
||||
};
|
||||
|
||||
const plan = await store.dispatch(`${ inStore }/create`, {
|
||||
type: HCI.FORKLIFT_PLAN,
|
||||
metadata: {
|
||||
name: planName,
|
||||
namespace: namespace.value,
|
||||
},
|
||||
spec: planSpec,
|
||||
});
|
||||
|
||||
await plan.save();
|
||||
|
||||
const migrationName = `${ planName }-migration-${ Math.random().toString(36).substring(2, 7) }`;
|
||||
|
||||
const migration = await store.dispatch(`${ inStore }/create`, {
|
||||
type: HCI.FORKLIFT_MIGRATION,
|
||||
metadata: {
|
||||
name: migrationName,
|
||||
namespace: namespace.value,
|
||||
},
|
||||
spec: {
|
||||
plan: {
|
||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||
kind: 'Plan',
|
||||
name: planName,
|
||||
namespace: namespace.value,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await migration.save();
|
||||
|
||||
router.push({
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
||||
params: {
|
||||
product: route.params.product,
|
||||
cluster: route.params.cluster,
|
||||
}
|
||||
});
|
||||
|
||||
buttonCb(true);
|
||||
} catch (err) {
|
||||
errors.value = [err.message || err];
|
||||
buttonCb(false);
|
||||
}
|
||||
};
|
||||
|
||||
const init = async() => {
|
||||
const vmsParam = route.query.vms;
|
||||
|
||||
if (vmsParam) {
|
||||
try {
|
||||
const vmIds = JSON.parse(vmsParam);
|
||||
|
||||
vms.value = vmIds.map((id) => {
|
||||
const found = vmData.find((vm) => vm.id === id);
|
||||
|
||||
return found || {
|
||||
id, name: id, networks: [], disks: [], cpuCount: 0, memoryMB: 0, guestName: ''
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
vms.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
const networkMappingsParam = route.query.networkMappings;
|
||||
|
||||
if (networkMappingsParam) {
|
||||
try {
|
||||
networkMappings.value = JSON.parse(networkMappingsParam);
|
||||
} catch (e) {
|
||||
networkMappings.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
const storageMappingsParam = route.query.storageMappings;
|
||||
|
||||
if (storageMappingsParam) {
|
||||
try {
|
||||
storageMappings.value = JSON.parse(storageMappingsParam);
|
||||
} catch (e) {
|
||||
storageMappings.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
return {
|
||||
schema,
|
||||
loading,
|
||||
vms,
|
||||
errors,
|
||||
providerName,
|
||||
namespace,
|
||||
totalVCpu,
|
||||
totalMemoryGB,
|
||||
totalStorageGB,
|
||||
vmCards,
|
||||
t,
|
||||
cancel,
|
||||
startMigration,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="loading" />
|
||||
<div
|
||||
v-else
|
||||
class="review-migration"
|
||||
>
|
||||
<Masthead
|
||||
:schema="schema"
|
||||
:resource="schema.id"
|
||||
:type-display="t('harvester.addons.forklift.reviewMigration.title')"
|
||||
:is-creatable="false"
|
||||
>
|
||||
<template #subHeader>
|
||||
<p class="text-muted mt-5">
|
||||
{{ t('harvester.addons.forklift.reviewMigration.description') }}
|
||||
</p>
|
||||
</template>
|
||||
</Masthead>
|
||||
|
||||
<!-- Migration Details Summary -->
|
||||
<div class="migration-details">
|
||||
<h3 class="section-title">
|
||||
{{ t('harvester.addons.forklift.reviewMigration.migrationDetails') }}
|
||||
</h3>
|
||||
<div class="details-grid">
|
||||
<div class="detail-column">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">{{ t('harvester.addons.forklift.reviewMigration.totalVms') }}</span>
|
||||
<span class="detail-value detail-value-large">{{ vms.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-column">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">{{ t('harvester.addons.forklift.reviewMigration.source') }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-value">{{ providerName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-column">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">{{ t('harvester.addons.forklift.reviewMigration.targetNamespace') }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-value">{{ namespace }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-column">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">{{ t('harvester.addons.forklift.reviewMigration.vcpu') }}</span>
|
||||
<span class="detail-value">{{ totalVCpu }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">{{ t('harvester.addons.forklift.reviewMigration.memory') }}</span>
|
||||
<span class="detail-value">{{ totalMemoryGB }} GB</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">{{ t('harvester.addons.forklift.reviewMigration.storage') }}</span>
|
||||
<span class="detail-value">{{ totalStorageGB }} GB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-column grid-column-2">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">{{ t('harvester.addons.forklift.reviewMigration.migrationMode') }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-value">
|
||||
{{ t('harvester.addons.forklift.reviewMigration.coldMigration') }}
|
||||
<span
|
||||
class="ml-10 small-text"
|
||||
>{{ t('harvester.addons.forklift.reviewMigration.vmsWillShutDown') }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Virtual Machines -->
|
||||
<div class="vm-section">
|
||||
<h3 class="section-title">
|
||||
{{ t('harvester.addons.forklift.reviewMigration.virtualMachines') }} ({{ vms.length }})
|
||||
</h3>
|
||||
|
||||
<div class="vm-cards-grid">
|
||||
<RcItemCard
|
||||
v-for="vm in vmCards"
|
||||
:id="vm.id"
|
||||
:key="vm.id"
|
||||
:variant="'small'"
|
||||
:header="{ title: { text: vm.name }, statuses: [{ icon: 'icon-notify-tick', color: 'text-success' }] }"
|
||||
>
|
||||
<template #item-card-content>
|
||||
<div class="vm-card-content">
|
||||
<div class="vm-card-specs">
|
||||
<span class="vm-os text-muted">{{ vm.os }}</span>
|
||||
<span class="vm-resources">
|
||||
<i class="icon icon-dock" />
|
||||
{{ vm.cpus }} vCPU • {{ vm.memGB }} • {{ vm.diskDisplay }}
|
||||
</span>
|
||||
</div>
|
||||
<MappingsCell
|
||||
:network-entries="vm.networkMappings.map(m => `${m.source} → ${m.target}`)"
|
||||
:storage-entries="vm.storageMappings.map(m => `${m.source} → ${m.target}`)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</RcItemCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cold Migration Warning -->
|
||||
<Banner
|
||||
color="warning"
|
||||
class="mt-20"
|
||||
>
|
||||
<div>
|
||||
<span class="banner-title">{{ t('harvester.addons.forklift.reviewMigration.warningTitle') }}</span><br>
|
||||
{{ t('harvester.addons.forklift.reviewMigration.warningMessage') }}
|
||||
</div>
|
||||
</Banner>
|
||||
|
||||
<!-- Error banner -->
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
color="error"
|
||||
:label="err"
|
||||
class="mt-10"
|
||||
/>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="actions-footer">
|
||||
<button
|
||||
class="btn role-secondary"
|
||||
@click="cancel"
|
||||
>
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
<AsyncButton
|
||||
:action-label="t('harvester.addons.forklift.reviewMigration.startMigration')"
|
||||
:waiting-label="t('harvester.addons.forklift.reviewMigration.startMigration')"
|
||||
:success-label="t('harvester.addons.forklift.reviewMigration.startMigration')"
|
||||
:error-label="t('harvester.addons.forklift.reviewMigration.startMigration')"
|
||||
@click="startMigration"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.review-migration {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: 600;
|
||||
margin: 0 0 15px 0;
|
||||
}
|
||||
|
||||
.migration-details {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(230px, 1fr) 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
gap: 12px 128px;
|
||||
width: 747px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.detail-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
&.grid-column-2 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
.detail-label {
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
|
||||
&.detail-value-large {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-section {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.vm-cards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.vm-card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.vm-card-specs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
.vm-os {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.vm-resources {
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-card-mappings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.mapping-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
.icon {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.small-text {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: #973C00;
|
||||
}
|
||||
|
||||
.actions-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.banner-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
</style>
|
||||
518
pkg/harvester/pages/c/_cluster/forklift/select-vms.vue
Normal file
518
pkg/harvester/pages/c/_cluster/forklift/select-vms.vue
Normal file
@ -0,0 +1,518 @@
|
||||
<script>
|
||||
import { defineComponent, ref, computed } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import Masthead from '@shell/components/ResourceList/Masthead';
|
||||
import SortableTable from '@shell/components/SortableTable';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import { SCHEMA } from '@shell/config/types';
|
||||
import { useI18n } from '@shell/composables/useI18n';
|
||||
import { HCI } from '../../../../types';
|
||||
import { PRODUCT_NAME } from '../../../../config/harvester';
|
||||
import vmData from '../../../../utils/vms.json';
|
||||
|
||||
const schema = {
|
||||
id: HCI.FORKLIFT_PROVIDER,
|
||||
type: SCHEMA,
|
||||
attributes: {
|
||||
kind: HCI.FORKLIFT_PROVIDER,
|
||||
namespaced: true
|
||||
},
|
||||
metadata: { name: HCI.FORKLIFT_PROVIDER },
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ForkliftSelectVms',
|
||||
|
||||
components: {
|
||||
Loading,
|
||||
Masthead,
|
||||
SortableTable,
|
||||
LabeledInput,
|
||||
},
|
||||
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n(store);
|
||||
|
||||
const allProviders = ref([]);
|
||||
const provider = ref(null);
|
||||
const discoveredVMs = ref([]);
|
||||
const selectedVMs = ref([]);
|
||||
const manualVMs = ref([]);
|
||||
const newVmId = ref('');
|
||||
const tableRows = ref([]);
|
||||
const loading = ref(true);
|
||||
|
||||
const providerName = computed(() => provider.value?.metadata?.name || route.query.provider || '');
|
||||
const vmCount = computed(() => discoveredVMs.value.length);
|
||||
const showManualEntry = computed(() => discoveredVMs.value.length === 0);
|
||||
const selectedCount = computed(() => showManualEntry.value ? manualVMs.value.length : selectedVMs.value.length);
|
||||
|
||||
const manualHeaders = [
|
||||
{
|
||||
name: 'id',
|
||||
labelKey: 'harvester.addons.forklift.selectVms.columns.vmId',
|
||||
value: 'id',
|
||||
},
|
||||
];
|
||||
|
||||
const headers = [
|
||||
{
|
||||
name: 'vmName',
|
||||
labelKey: 'harvester.addons.forklift.selectVms.columns.vmName',
|
||||
value: 'vmName',
|
||||
subLabel: 'Identifier',
|
||||
},
|
||||
{
|
||||
name: 'os',
|
||||
labelKey: 'harvester.addons.forklift.selectVms.columns.os',
|
||||
value: 'os',
|
||||
},
|
||||
{
|
||||
name: 'resources',
|
||||
labelKey: 'harvester.addons.forklift.selectVms.columns.resources',
|
||||
value: 'resources',
|
||||
sort: false,
|
||||
},
|
||||
{
|
||||
name: 'powerState',
|
||||
labelKey: 'harvester.addons.forklift.selectVms.columns.powerState',
|
||||
value: 'powerState',
|
||||
},
|
||||
{
|
||||
name: 'network',
|
||||
labelKey: 'harvester.addons.forklift.selectVms.columns.network',
|
||||
value: 'network',
|
||||
subLabel: 'Identifier',
|
||||
},
|
||||
{
|
||||
name: 'datastore',
|
||||
labelKey: 'harvester.addons.forklift.selectVms.columns.datastore',
|
||||
value: 'datastore',
|
||||
subLabel: 'Identifier',
|
||||
},
|
||||
];
|
||||
|
||||
const buildTableRows = () => {
|
||||
return discoveredVMs.value.map((vm) => {
|
||||
const cpus = vm.cpuCount || vm.numCPU || '-';
|
||||
const memMB = vm.memoryMB || vm.memory || 0;
|
||||
const memGB = memMB ? `${ Math.round(memMB / 1024) } GB` : '-';
|
||||
|
||||
let totalDiskBytes = 0;
|
||||
|
||||
if (vm.disks && vm.disks.length > 0) {
|
||||
totalDiskBytes = vm.disks.reduce((sum, d) => sum + (d.capacity || 0), 0);
|
||||
}
|
||||
|
||||
const diskDisplay = totalDiskBytes ? `${ Math.round(totalDiskBytes / (1024 * 1024 * 1024)) } GB` : '-';
|
||||
const powerState = vm.powerState || vm.status?.phase || '-';
|
||||
|
||||
let networks = [];
|
||||
|
||||
if (vm.networks && vm.networks.length > 0) {
|
||||
networks = vm.networks.map((n) => ({
|
||||
name: n.name || n.id || n,
|
||||
id: n.id || '',
|
||||
vlanId: n.vlanId || '',
|
||||
}));
|
||||
}
|
||||
|
||||
const datastores = [];
|
||||
|
||||
if (vm.disks && vm.disks.length > 0) {
|
||||
const seen = new Set();
|
||||
|
||||
vm.disks.forEach((d) => {
|
||||
const dsId = d.datastore?.id || d.datastore?.name;
|
||||
|
||||
if (dsId && !seen.has(dsId)) {
|
||||
seen.add(dsId);
|
||||
datastores.push({
|
||||
name: d.datastore?.name || dsId,
|
||||
id: d.datastore?.id || '',
|
||||
type: d.datastore?.type || '',
|
||||
capacity: d.capacity || 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
_original: vm,
|
||||
_key: vm.id || vm.vmId || vm.metadata?.name,
|
||||
vmName: vm.name || vm.metadata?.name || '-',
|
||||
vmId: vm.id || vm.vmId || vm.metadata?.name || '-',
|
||||
os: vm.guestName || vm.guestOS || vm.os || '-',
|
||||
resourcesDisplay: `${ cpus } vCPU`,
|
||||
resourcesSub: `${ memGB } • ${ diskDisplay }`,
|
||||
powerState,
|
||||
powerStateClass: powerState.toLowerCase().includes('on') || powerState.toLowerCase().includes('running') ? 'power-on' : 'power-off',
|
||||
networks,
|
||||
network: networks.map((n) => n.name).join(', ') || '-',
|
||||
datastores,
|
||||
datastore: datastores.map((d) => d.name).join(', ') || '-',
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const addManualVm = () => {
|
||||
if (!newVmId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
manualVMs.value.push({
|
||||
id: newVmId.value.trim(),
|
||||
name: newVmId.value.trim(),
|
||||
_key: `manual-${ newVmId.value.trim() }`,
|
||||
});
|
||||
|
||||
newVmId.value = '';
|
||||
};
|
||||
|
||||
const removeManualVm = (index) => {
|
||||
manualVMs.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const onSelect = (rows) => {
|
||||
selectedVMs.value = rows.map((r) => r._original);
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
router.push({
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
||||
params: {
|
||||
product: route.params.product,
|
||||
cluster: route.params.cluster,
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const saveSelection = () => {
|
||||
let vmIds;
|
||||
|
||||
if (showManualEntry.value) {
|
||||
vmIds = manualVMs.value.map((vm) => vm.id);
|
||||
} else {
|
||||
vmIds = selectedVMs.value.map((vm) => vm.id || vm.vmId || vm.metadata?.name);
|
||||
}
|
||||
|
||||
router.push({
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-configure-mappings`,
|
||||
params: {
|
||||
product: route.params.product,
|
||||
cluster: route.params.cluster,
|
||||
},
|
||||
query: {
|
||||
provider: providerName.value,
|
||||
namespace: provider.value?.metadata?.namespace || 'default',
|
||||
vms: JSON.stringify(vmIds),
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const init = async() => {
|
||||
const inStore = store.getters['currentProduct'].inStore;
|
||||
|
||||
allProviders.value = await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
|
||||
|
||||
const queryProvider = route.query.provider;
|
||||
const namespace = route.query.namespace || 'default';
|
||||
|
||||
if (queryProvider) {
|
||||
provider.value = allProviders.value.find(
|
||||
(p) => p.metadata.name === queryProvider && p.metadata.namespace === namespace
|
||||
);
|
||||
}
|
||||
|
||||
if (provider.value) {
|
||||
try {
|
||||
const inventoryType = 'forklift-inventory.konveyor.io.vspherevm';
|
||||
|
||||
discoveredVMs.value = await store.dispatch(`${ inStore }/findAll`, { type: inventoryType });
|
||||
} catch (e) {
|
||||
discoveredVMs.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (discoveredVMs.value.length === 0) {
|
||||
discoveredVMs.value = vmData;
|
||||
}
|
||||
|
||||
tableRows.value = buildTableRows();
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
return {
|
||||
schema,
|
||||
loading,
|
||||
provider,
|
||||
providerName,
|
||||
discoveredVMs,
|
||||
selectedVMs,
|
||||
manualVMs,
|
||||
newVmId,
|
||||
tableRows,
|
||||
vmCount,
|
||||
showManualEntry,
|
||||
selectedCount,
|
||||
manualHeaders,
|
||||
headers,
|
||||
t,
|
||||
addManualVm,
|
||||
removeManualVm,
|
||||
onSelect,
|
||||
cancel,
|
||||
saveSelection,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="loading" />
|
||||
<div
|
||||
v-else
|
||||
>
|
||||
<Masthead
|
||||
:schema="schema"
|
||||
:resource="schema.id"
|
||||
:type-display="t('harvester.addons.forklift.selectVms.title')"
|
||||
:is-creatable="false"
|
||||
>
|
||||
<template #subHeader>
|
||||
<p class="text-muted mmt-5">
|
||||
{{ t('harvester.addons.forklift.selectVms.discovered', { count: vmCount }) }}
|
||||
<a
|
||||
v-if="providerName"
|
||||
class="provider-link"
|
||||
>{{ providerName }}</a>
|
||||
<br>
|
||||
<span class="text-small">{{ t('harvester.addons.forklift.selectVms.lastSynced') }}</span>
|
||||
</p>
|
||||
</template>
|
||||
</Masthead>
|
||||
|
||||
<!-- Manual VM entry when no discovered VMs -->
|
||||
<div v-if="showManualEntry">
|
||||
<h3 class="m-0 mb-10">
|
||||
{{ t('harvester.addons.forklift.selectVms.manualEntry.title') }}
|
||||
</h3>
|
||||
<p class="text-muted mb-20">
|
||||
{{ t('harvester.addons.forklift.selectVms.manualEntry.description') }}
|
||||
</p>
|
||||
|
||||
<div class="manual-vm-form">
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="newVmId"
|
||||
:label="t('harvester.addons.forklift.selectVms.manualEntry.vmId')"
|
||||
:placeholder="t('harvester.addons.forklift.selectVms.manualEntry.vmIdPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-2 add-btn-col">
|
||||
<button
|
||||
class="btn role-primary"
|
||||
:disabled="!newVmId"
|
||||
@click="addManualVm"
|
||||
>
|
||||
{{ t('harvester.addons.forklift.selectVms.manualEntry.add') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SortableTable
|
||||
:rows="manualVMs"
|
||||
:headers="manualHeaders"
|
||||
:search="false"
|
||||
:table-actions="false"
|
||||
:row-actions="false"
|
||||
:groupable="false"
|
||||
key-field="_key"
|
||||
class="mt-20"
|
||||
>
|
||||
<template #header-left>
|
||||
<span class="text-muted">{{ manualVMs.length }} {{ t('harvester.addons.forklift.selectVms.manualEntry.added') }}</span>
|
||||
</template>
|
||||
<template #cell:id="{ row }">
|
||||
<div class="manual-vm-row">
|
||||
<span>{{ row.id }}</span>
|
||||
<button
|
||||
class="btn btn-sm role-link"
|
||||
@click="removeManualVm(manualVMs.indexOf(row))"
|
||||
>
|
||||
<i class="icon icon-close" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</SortableTable>
|
||||
</div>
|
||||
|
||||
<!-- Discovered VMs table -->
|
||||
<SortableTable
|
||||
v-else
|
||||
:rows="tableRows"
|
||||
:headers="headers"
|
||||
:search="true"
|
||||
:table-actions="true"
|
||||
:row-actions="false"
|
||||
:groupable="false"
|
||||
key-field="_key"
|
||||
@selection="onSelect"
|
||||
>
|
||||
<template #header-left>
|
||||
<div class="vm-table-title">
|
||||
<h3 class="m-0">
|
||||
{{ t('harvester.addons.forklift.selectVms.availableVms') }}
|
||||
</h3>
|
||||
<span class="text-muted">{{ selectedCount }} {{ t('harvester.addons.forklift.selectVms.selected') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell:vmName="{ row }">
|
||||
<div class="vm-name-cell">
|
||||
<span class="vm-name">{{ row.vmName }}</span>
|
||||
<span class="vm-id text-muted">{{ row.vmId }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell:resources="{ row }">
|
||||
<span>{{ row.resourcesDisplay }}</span><br>
|
||||
<span class="text-muted">{{ row.resourcesSub }}</span>
|
||||
</template>
|
||||
<template #cell:powerState="{ row }">
|
||||
<span
|
||||
class="power-badge"
|
||||
:class="row.powerStateClass"
|
||||
>{{ row.powerState }}</span>
|
||||
</template>
|
||||
<template #cell:network="{ row }">
|
||||
<div>
|
||||
<div
|
||||
v-for="(net, i) in row.networks"
|
||||
:key="i"
|
||||
:class="{ 'mt-4': i > 0 }"
|
||||
>
|
||||
<span>{{ net.name }}</span><br>
|
||||
<span class="text-muted">{{ net.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell:datastore="{ row }">
|
||||
<div>
|
||||
<div
|
||||
v-for="(ds, i) in row.datastores"
|
||||
:key="i"
|
||||
:class="{ 'mt-4': i > 0 }"
|
||||
>
|
||||
<span>{{ ds.name }}</span><br>
|
||||
<span class="text-muted">{{ ds.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</SortableTable>
|
||||
|
||||
<div class="actions-footer">
|
||||
<button
|
||||
class="btn role-secondary"
|
||||
@click="cancel"
|
||||
>
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn role-primary"
|
||||
:disabled="selectedCount === 0"
|
||||
@click="saveSelection"
|
||||
>
|
||||
{{ t('harvester.addons.forklift.selectVms.saveSelection') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.provider-link {
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.vm-table-title {
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-name-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.vm-name {
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.vm-id {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.power-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
|
||||
&.power-on {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
&.power-off {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
}
|
||||
|
||||
.manual-vm-form {
|
||||
.add-btn-col {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.manual-vm-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.multi-line-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.actions-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
</style>
|
||||
@ -16,6 +16,11 @@ import HarvesterMembers from '../pages/c/_cluster/members/index.vue';
|
||||
import ProjectNamespaces from '../pages/c/_cluster/projectsnamespaces.vue';
|
||||
import HarvesterAlertmanagerReceiver from '../pages/c/_cluster/alertmanagerconfig/_alertmanagerconfigid/receiver.vue';
|
||||
import HarvesterUnsupported from '../pages/c/_cluster/unsupported/index.vue';
|
||||
import ForkliftDashboard from '../pages/c/_cluster/forklift/index.vue';
|
||||
import ForkliftConfigureProvider from '../pages/c/_cluster/forklift/configure-provider.vue';
|
||||
import ForkliftSelectVms from '../pages/c/_cluster/forklift/select-vms.vue';
|
||||
import ForkliftConfigureMappings from '../pages/c/_cluster/forklift/configure-mappings.vue';
|
||||
import ForkliftReviewMigration from '../pages/c/_cluster/forklift/review-migration.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@ -83,6 +88,26 @@ const routes = [
|
||||
name: `${ PRODUCT_NAME }-c-cluster-projectsnamespaces`,
|
||||
path: `/:product/c/:cluster/projectsnamespaces`,
|
||||
component: ProjectNamespaces,
|
||||
}, {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
||||
path: `/:product/c/:cluster/forklift`,
|
||||
component: ForkliftDashboard,
|
||||
}, {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-configure-provider`,
|
||||
path: `/:product/c/:cluster/forklift/configure-provider`,
|
||||
component: ForkliftConfigureProvider,
|
||||
}, {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-select-vms`,
|
||||
path: `/:product/c/:cluster/forklift/select-vms`,
|
||||
component: ForkliftSelectVms,
|
||||
}, {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-configure-mappings`,
|
||||
path: `/:product/c/:cluster/forklift/configure-mappings`,
|
||||
component: ForkliftConfigureMappings,
|
||||
}, {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-review-migration`,
|
||||
path: `/:product/c/:cluster/forklift/review-migration`,
|
||||
component: ForkliftReviewMigration,
|
||||
}, {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
path: `/:product/c/:cluster/:resource`,
|
||||
|
||||
272
pkg/harvester/utils/datastores.json
Normal file
272
pkg/harvester/utils/datastores.json
Normal file
@ -0,0 +1,272 @@
|
||||
[
|
||||
{
|
||||
"id": "datastore-16879",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-s1004"
|
||||
},
|
||||
"path": "/PG Devlab/datastore/Content Library Datastore",
|
||||
"revision": 646,
|
||||
"name": "Content Library Datastore",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/datastores/datastore-16879",
|
||||
"type": "NFS41",
|
||||
"capacity": 65542688866304,
|
||||
"free": 65166035648512,
|
||||
"maintenance": "normal"
|
||||
},
|
||||
{
|
||||
"id": "datastore-2036",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-s1004"
|
||||
},
|
||||
"path": "/PG Devlab/datastore/Mako14-Spinner1",
|
||||
"revision": 460,
|
||||
"name": "Mako14-Spinner1",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/datastores/datastore-2036",
|
||||
"type": "VMFS",
|
||||
"capacity": 0,
|
||||
"free": 0,
|
||||
"maintenance": "normal"
|
||||
},
|
||||
{
|
||||
"id": "datastore-16880",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-s1004"
|
||||
},
|
||||
"path": "/PG Devlab/datastore/VCSA Services",
|
||||
"revision": 631,
|
||||
"name": "VCSA Services",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/datastores/datastore-16880",
|
||||
"type": "NFS41",
|
||||
"capacity": 1545487384576,
|
||||
"free": 1545007792128,
|
||||
"maintenance": "normal"
|
||||
},
|
||||
{
|
||||
"id": "datastore-12038",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-s1004"
|
||||
},
|
||||
"path": "/PG Devlab/datastore/Mako13-Spinner1",
|
||||
"revision": 1,
|
||||
"name": "Mako13-Spinner1",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/datastores/datastore-12038",
|
||||
"type": "VMFS",
|
||||
"capacity": 8001524072448,
|
||||
"free": 7993813893120,
|
||||
"maintenance": "normal"
|
||||
},
|
||||
{
|
||||
"id": "datastore-2045",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-s1004"
|
||||
},
|
||||
"path": "/PG Devlab/datastore/Mako15-Spinner1",
|
||||
"revision": 10,
|
||||
"name": "Mako15-Spinner1",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/datastores/datastore-2045",
|
||||
"type": "VMFS",
|
||||
"capacity": 8001524072448,
|
||||
"free": 7883079024640,
|
||||
"maintenance": "normal"
|
||||
},
|
||||
{
|
||||
"id": "datastore-2043",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-s1004"
|
||||
},
|
||||
"path": "/PG Devlab/datastore/Mako13-Local",
|
||||
"revision": 107,
|
||||
"name": "Mako13-Local",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/datastores/datastore-2043",
|
||||
"type": "VMFS",
|
||||
"capacity": 8001524072448,
|
||||
"free": 7444761673728,
|
||||
"maintenance": "normal"
|
||||
},
|
||||
{
|
||||
"id": "datastore-2044",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-s1004"
|
||||
},
|
||||
"path": "/PG Devlab/datastore/Mako14-Local",
|
||||
"revision": 411,
|
||||
"name": "Mako14-Local",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/datastores/datastore-2044",
|
||||
"type": "VMFS",
|
||||
"capacity": 8001524072448,
|
||||
"free": 7953250779136,
|
||||
"maintenance": "normal"
|
||||
},
|
||||
{
|
||||
"id": "datastore-27001",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-s1004"
|
||||
},
|
||||
"path": "/PG Devlab/datastore/Heartbeat-01",
|
||||
"revision": 644,
|
||||
"name": "Heartbeat-01",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/datastores/datastore-27001",
|
||||
"type": "NFS41",
|
||||
"capacity": 1545227599872,
|
||||
"free": 1545007792128,
|
||||
"maintenance": "normal"
|
||||
},
|
||||
{
|
||||
"id": "datastore-27002",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-s1004"
|
||||
},
|
||||
"path": "/PG Devlab/datastore/Heartbeat-02",
|
||||
"revision": 642,
|
||||
"name": "Heartbeat-02",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/datastores/datastore-27002",
|
||||
"type": "NFS41",
|
||||
"capacity": 1545007972352,
|
||||
"free": 1545007792128,
|
||||
"maintenance": "normal"
|
||||
},
|
||||
{
|
||||
"id": "datastore-60001",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-s1004"
|
||||
},
|
||||
"path": "/PG Devlab/datastore/NFS VM Datastore",
|
||||
"revision": 460,
|
||||
"name": "NFS VM Datastore",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/datastores/datastore-60001",
|
||||
"type": "NFS41",
|
||||
"capacity": 65169610178560,
|
||||
"free": 65166043054080,
|
||||
"maintenance": "normal"
|
||||
},
|
||||
{
|
||||
"id": "datastore-14753",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-s1004"
|
||||
},
|
||||
"path": "/PG Devlab/datastore/Content Library Datastore-DEAD",
|
||||
"revision": 1,
|
||||
"name": "Content Library Datastore-DEAD",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/datastores/datastore-14753",
|
||||
"type": "NFS41",
|
||||
"capacity": 76523271618560,
|
||||
"free": 76438520856576,
|
||||
"maintenance": "normal"
|
||||
},
|
||||
{
|
||||
"id": "datastore-2039",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-s1004"
|
||||
},
|
||||
"path": "/PG Devlab/datastore/Mako15-Local",
|
||||
"revision": 263,
|
||||
"name": "Mako15-Local",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/datastores/datastore-2039",
|
||||
"type": "VMFS",
|
||||
"capacity": 8001524072448,
|
||||
"free": 7828817313792,
|
||||
"maintenance": "normal"
|
||||
},
|
||||
{
|
||||
"id": "datastore-2035",
|
||||
"parent": {
|
||||
"kind": "StoragePod",
|
||||
"id": "group-p2042"
|
||||
},
|
||||
"path": "/Mako13-NVMe2",
|
||||
"revision": 855,
|
||||
"name": "Mako13-NVMe2",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/datastores/datastore-2035",
|
||||
"type": "VMFS",
|
||||
"capacity": 1600143753216,
|
||||
"free": 443096760320,
|
||||
"maintenance": "normal"
|
||||
},
|
||||
{
|
||||
"id": "datastore-2034",
|
||||
"parent": {
|
||||
"kind": "StoragePod",
|
||||
"id": "group-p2042"
|
||||
},
|
||||
"path": "/Mako13-NVMe1",
|
||||
"revision": 44,
|
||||
"name": "Mako13-NVMe1",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/datastores/datastore-2034",
|
||||
"type": "VMFS",
|
||||
"capacity": 1600143753216,
|
||||
"free": 350873452544,
|
||||
"maintenance": "normal"
|
||||
},
|
||||
{
|
||||
"id": "datastore-2037",
|
||||
"parent": {
|
||||
"kind": "StoragePod",
|
||||
"id": "group-p2042"
|
||||
},
|
||||
"path": "/Mako14-NVMe1",
|
||||
"revision": 450,
|
||||
"name": "Mako14-NVMe1",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/datastores/datastore-2037",
|
||||
"type": "VMFS",
|
||||
"capacity": 1600143753216,
|
||||
"free": 466680283136,
|
||||
"maintenance": "normal"
|
||||
},
|
||||
{
|
||||
"id": "datastore-2040",
|
||||
"parent": {
|
||||
"kind": "StoragePod",
|
||||
"id": "group-p2042"
|
||||
},
|
||||
"path": "/Mako15-NVMe1",
|
||||
"revision": 648,
|
||||
"name": "Mako15-NVMe1",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/datastores/datastore-2040",
|
||||
"type": "VMFS",
|
||||
"capacity": 1600143753216,
|
||||
"free": 380083634176,
|
||||
"maintenance": "normal"
|
||||
},
|
||||
{
|
||||
"id": "datastore-2038",
|
||||
"parent": {
|
||||
"kind": "StoragePod",
|
||||
"id": "group-p2042"
|
||||
},
|
||||
"path": "/Mako14-NVMe2",
|
||||
"revision": 623,
|
||||
"name": "Mako14-NVMe2",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/datastores/datastore-2038",
|
||||
"type": "VMFS",
|
||||
"capacity": 1600143753216,
|
||||
"free": 685895581696,
|
||||
"maintenance": "normal"
|
||||
},
|
||||
{
|
||||
"id": "datastore-2041",
|
||||
"parent": {
|
||||
"kind": "StoragePod",
|
||||
"id": "group-p2042"
|
||||
},
|
||||
"path": "/Mako15-NVMe2",
|
||||
"revision": 642,
|
||||
"name": "Mako15-NVMe2",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/datastores/datastore-2041",
|
||||
"type": "VMFS",
|
||||
"capacity": 1600143753216,
|
||||
"free": 383375114240,
|
||||
"maintenance": "normal"
|
||||
}
|
||||
]
|
||||
332
pkg/harvester/utils/networks.json
Normal file
332
pkg/harvester/utils/networks.json
Normal file
@ -0,0 +1,332 @@
|
||||
[
|
||||
{
|
||||
"id": "dvs-5001",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-n1005"
|
||||
},
|
||||
"path": "/PG Devlab/network/Production-VDS",
|
||||
"revision": 1,
|
||||
"name": "Production-VDS",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/networks/dvs-5001",
|
||||
"variant": "DvSwitch",
|
||||
"vlanId": "",
|
||||
"host": [
|
||||
{
|
||||
"Host": {
|
||||
"kind": "Host",
|
||||
"id": "host-1010"
|
||||
},
|
||||
"PNIC": [
|
||||
"vmnic2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Host": {
|
||||
"kind": "Host",
|
||||
"id": "host-1012"
|
||||
},
|
||||
"PNIC": [
|
||||
"vmnic2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Host": {
|
||||
"kind": "Host",
|
||||
"id": "host-2003"
|
||||
},
|
||||
"PNIC": [
|
||||
"vmnic2"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dvs-13021",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-n1005"
|
||||
},
|
||||
"path": "/PG Devlab/network/DSwitch",
|
||||
"revision": 1,
|
||||
"name": "DSwitch",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/networks/dvs-13021",
|
||||
"variant": "DvSwitch",
|
||||
"vlanId": "",
|
||||
"host": [
|
||||
{
|
||||
"Host": {
|
||||
"kind": "Host",
|
||||
"id": "host-1010"
|
||||
},
|
||||
"PNIC": [
|
||||
"vmnic0"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Host": {
|
||||
"kind": "Host",
|
||||
"id": "host-1012"
|
||||
},
|
||||
"PNIC": [
|
||||
"vmnic0"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Host": {
|
||||
"kind": "Host",
|
||||
"id": "host-2003"
|
||||
},
|
||||
"PNIC": [
|
||||
"vmnic0"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dvportgroup-13026",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-n1005"
|
||||
},
|
||||
"path": "/PG Devlab/network/DSwitch-vMotion",
|
||||
"revision": 1,
|
||||
"name": "DSwitch-vMotion",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/networks/dvportgroup-13026",
|
||||
"variant": "DvPortGroup",
|
||||
"dvSwitch": {
|
||||
"kind": "Network",
|
||||
"id": "dvs-13021"
|
||||
},
|
||||
"vlanId": "",
|
||||
"host": null,
|
||||
"key": "dvportgroup-13026"
|
||||
},
|
||||
{
|
||||
"id": "dvportgroup-13027",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-n1005"
|
||||
},
|
||||
"path": "/PG Devlab/network/DSwitch-vSAN",
|
||||
"revision": 1,
|
||||
"name": "DSwitch-vSAN",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/networks/dvportgroup-13027",
|
||||
"variant": "DvPortGroup",
|
||||
"dvSwitch": {
|
||||
"kind": "Network",
|
||||
"id": "dvs-13021"
|
||||
},
|
||||
"vlanId": "",
|
||||
"host": null,
|
||||
"key": "dvportgroup-13027"
|
||||
},
|
||||
{
|
||||
"id": "dvportgroup-13023",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-n1005"
|
||||
},
|
||||
"path": "/PG Devlab/network/DSwitch-vCenter-HA-Management",
|
||||
"revision": 1,
|
||||
"name": "DSwitch-vCenter-HA-Management",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/networks/dvportgroup-13023",
|
||||
"variant": "DvPortGroup",
|
||||
"dvSwitch": {
|
||||
"kind": "Network",
|
||||
"id": "dvs-13021"
|
||||
},
|
||||
"vlanId": "",
|
||||
"host": null,
|
||||
"key": "dvportgroup-13023"
|
||||
},
|
||||
{
|
||||
"id": "dvportgroup-13025",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-n1005"
|
||||
},
|
||||
"path": "/PG Devlab/network/DSwitch-vCenter-HA-VM-Network",
|
||||
"revision": 1,
|
||||
"name": "DSwitch-vCenter-HA-VM-Network",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/networks/dvportgroup-13025",
|
||||
"variant": "DvPortGroup",
|
||||
"dvSwitch": {
|
||||
"kind": "Network",
|
||||
"id": "dvs-13021"
|
||||
},
|
||||
"vlanId": "",
|
||||
"host": null,
|
||||
"key": "dvportgroup-13025"
|
||||
},
|
||||
{
|
||||
"id": "dvportgroup-13024",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-n1005"
|
||||
},
|
||||
"path": "/PG Devlab/network/DSwitch-vCenter-HA-Witness",
|
||||
"revision": 1,
|
||||
"name": "DSwitch-vCenter-HA-Witness",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/networks/dvportgroup-13024",
|
||||
"variant": "DvPortGroup",
|
||||
"dvSwitch": {
|
||||
"kind": "Network",
|
||||
"id": "dvs-13021"
|
||||
},
|
||||
"vlanId": "",
|
||||
"host": null,
|
||||
"key": "dvportgroup-13024"
|
||||
},
|
||||
{
|
||||
"id": "dvportgroup-5002",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-n1005"
|
||||
},
|
||||
"path": "/PG Devlab/network/Production-VDS-DVUplinks-5001",
|
||||
"revision": 1,
|
||||
"name": "Production-VDS-DVUplinks-5001",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/networks/dvportgroup-5002",
|
||||
"variant": "DvPortGroup",
|
||||
"dvSwitch": {
|
||||
"kind": "Network",
|
||||
"id": "dvs-5001"
|
||||
},
|
||||
"vlanId": "0-4094",
|
||||
"host": null,
|
||||
"key": "dvportgroup-5002"
|
||||
},
|
||||
{
|
||||
"id": "dvportgroup-5003",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-n1005"
|
||||
},
|
||||
"path": "/PG Devlab/network/VM-Network",
|
||||
"revision": 1,
|
||||
"name": "VM-Network",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/networks/dvportgroup-5003",
|
||||
"variant": "DvPortGroup",
|
||||
"dvSwitch": {
|
||||
"kind": "Network",
|
||||
"id": "dvs-5001"
|
||||
},
|
||||
"vlanId": "",
|
||||
"host": null,
|
||||
"key": "dvportgroup-5003"
|
||||
},
|
||||
{
|
||||
"id": "dvportgroup-5005",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-n1005"
|
||||
},
|
||||
"path": "/PG Devlab/network/mgmt",
|
||||
"revision": 1,
|
||||
"name": "mgmt",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/networks/dvportgroup-5005",
|
||||
"variant": "DvPortGroup",
|
||||
"dvSwitch": {
|
||||
"kind": "Network",
|
||||
"id": "dvs-5001"
|
||||
},
|
||||
"vlanId": "",
|
||||
"host": null,
|
||||
"key": "dvportgroup-5005"
|
||||
},
|
||||
{
|
||||
"id": "dvportgroup-6001",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-n1005"
|
||||
},
|
||||
"path": "/PG Devlab/network/vCenter-HA",
|
||||
"revision": 1,
|
||||
"name": "vCenter-HA",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/networks/dvportgroup-6001",
|
||||
"variant": "DvPortGroup",
|
||||
"dvSwitch": {
|
||||
"kind": "Network",
|
||||
"id": "dvs-5001"
|
||||
},
|
||||
"vlanId": "",
|
||||
"host": null,
|
||||
"key": "dvportgroup-6001"
|
||||
},
|
||||
{
|
||||
"id": "dvportgroup-13022",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-n1005"
|
||||
},
|
||||
"path": "/PG Devlab/network/DSwitch-DVUplinks-13021",
|
||||
"revision": 1,
|
||||
"name": "DSwitch-DVUplinks-13021",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/networks/dvportgroup-13022",
|
||||
"variant": "DvPortGroup",
|
||||
"dvSwitch": {
|
||||
"kind": "Network",
|
||||
"id": "dvs-13021"
|
||||
},
|
||||
"vlanId": "0-4094",
|
||||
"host": null,
|
||||
"key": "dvportgroup-13022"
|
||||
},
|
||||
{
|
||||
"id": "network-12023",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-n1005"
|
||||
},
|
||||
"path": "/PG Devlab/network/vCenter-HA-Witness",
|
||||
"revision": 1,
|
||||
"name": "vCenter-HA-Witness",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/networks/network-12023",
|
||||
"variant": "Standard",
|
||||
"vlanId": "",
|
||||
"host": null
|
||||
},
|
||||
{
|
||||
"id": "network-12022",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-n1005"
|
||||
},
|
||||
"path": "/PG Devlab/network/vCenter-HA-Management",
|
||||
"revision": 1,
|
||||
"name": "vCenter-HA-Management",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/networks/network-12022",
|
||||
"variant": "Standard",
|
||||
"vlanId": "",
|
||||
"host": null
|
||||
},
|
||||
{
|
||||
"id": "network-12021",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-n1005"
|
||||
},
|
||||
"path": "/PG Devlab/network/vCenter-HA-VM-Network",
|
||||
"revision": 1,
|
||||
"name": "vCenter-HA-VM-Network",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/networks/network-12021",
|
||||
"variant": "Standard",
|
||||
"vlanId": "",
|
||||
"host": null
|
||||
},
|
||||
{
|
||||
"id": "network-1017",
|
||||
"parent": {
|
||||
"kind": "Folder",
|
||||
"id": "group-n1005"
|
||||
},
|
||||
"path": "/PG Devlab/network/VM Network",
|
||||
"revision": 1,
|
||||
"name": "VM Network",
|
||||
"selfLink": "providers/vsphere/eaac340a-8953-479d-9463-85a1a100ab9d/networks/network-1017",
|
||||
"variant": "Standard",
|
||||
"vlanId": "",
|
||||
"host": null
|
||||
}
|
||||
]
|
||||
14275
pkg/harvester/utils/vms.json
Normal file
14275
pkg/harvester/utils/vms.json
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user