Skip to content

Commit 4fc834a

Browse files
committed
feat(vmimport): Add KVM source
Related to: harvester/harvester#9948 Signed-off-by: Volker Theile <vtheile@suse.com>
1 parent 708a95b commit 4fc834a

7 files changed

Lines changed: 344 additions & 0 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"build": "./node_modules/.bin/vue-cli-service build",
3939
"clean": "./node_modules/@rancher/shell/scripts/clean",
4040
"lint": "./node_modules/.bin/eslint --max-warnings 0 --ext .js,.ts,.vue .",
41+
"lint:fix": "./node_modules/.bin/eslint --fix --max-warnings 0 --ext .js,.ts,.vue .",
4142
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
4243
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
4344
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",

pkg/harvester/config/harvester-cluster.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ import {
4545
VM_IMPORT_SOURCE_O_REGION,
4646
VM_IMPORT_SOURCE_O_ENDPOINT,
4747
VM_IMPORT_SOURCE_O_STATUS,
48+
VM_IMPORT_SOURCE_KVM_URI,
49+
VM_IMPORT_SOURCE_KVM_STATUS,
4850
VM_IMPORT_SOURCE_OVA_URL,
4951
VM_IMPORT_SOURCE_OVA_STATUS,
5052
} from './table-headers';
@@ -302,6 +304,34 @@ export function init($plugin, store) {
302304
}
303305
});
304306

307+
// Source: KVM
308+
headers(HCI.VMIMPORT_SOURCE_KVM, [
309+
STATE,
310+
NAME_COL,
311+
VM_IMPORT_SOURCE_KVM_URI,
312+
VM_IMPORT_SOURCE_KVM_STATUS,
313+
AGE
314+
]);
315+
configureType(HCI.VMIMPORT_SOURCE_KVM, {
316+
resource: HCI.VMIMPORT_SOURCE_KVM,
317+
resourceDetail: HCI.VMIMPORT_SOURCE_KVM,
318+
resourceEdit: HCI.VMIMPORT_SOURCE_KVM,
319+
location: {
320+
name: `${ PRODUCT_NAME }-c-cluster-resource`,
321+
params: { resource: HCI.VMIMPORT_SOURCE_KVM }
322+
}
323+
});
324+
virtualType({
325+
name: HCI.VMIMPORT_SOURCE_KVM,
326+
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceKVM',
327+
group: 'vmimport',
328+
namespaced: true,
329+
route: {
330+
name: `${ PRODUCT_NAME }-c-cluster-resource`,
331+
params: { resource: HCI.VMIMPORT_SOURCE_KVM }
332+
}
333+
});
334+
305335
// Source: OVA
306336
headers(HCI.VMIMPORT_SOURCE_OVA, [
307337
STATE,
@@ -338,6 +368,7 @@ export function init($plugin, store) {
338368
types: [
339369
HCI.VMIMPORT_SOURCE_V,
340370
HCI.VMIMPORT_SOURCE_O,
371+
HCI.VMIMPORT_SOURCE_KVM,
341372
HCI.VMIMPORT_SOURCE_OVA,
342373
HCI.VMIMPORT
343374
]

pkg/harvester/config/table-headers.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,24 @@ export const VM_IMPORT_SOURCE_O_STATUS = {
213213
align: 'left',
214214
};
215215

216+
// URI column in migration.harvesterhci.io.kvmsource list page
217+
export const VM_IMPORT_SOURCE_KVM_URI = {
218+
name: 'url',
219+
labelKey: 'harvester.tableHeaders.vmImportSourceKVMUri',
220+
value: 'spec.libvirtURI',
221+
sort: 'spec.libvirtURI',
222+
align: 'left',
223+
};
224+
225+
// Status column in migration.harvesterhci.io.kvmsource list page
226+
export const VM_IMPORT_SOURCE_KVM_STATUS = {
227+
name: 'status',
228+
labelKey: 'harvester.tableHeaders.vmImportSourceKVMStatus',
229+
value: 'status.status',
230+
sort: 'status.status',
231+
align: 'left',
232+
};
233+
216234
// URL column in migration.harvesterhci.io.ovasource list page
217235
export const VM_IMPORT_SOURCE_OVA_URL = {
218236
name: 'url',

pkg/harvester/config/types.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ export const L2VLAN_MODE = {
3333
export const VMIMPORT_SOURCE_PROVIDER = {
3434
VMWARE: 'vmware',
3535
OPENSTACK: 'openstack',
36+
KVM: 'kvm',
3637
OVA: 'ova',
3738
};
3839

3940
export const VMIMPORT_SOURCE_KINDS = {
4041
VMWARE: 'VmwareSource',
4142
OPENSTACK: 'OpenstackSource',
43+
KVM: 'KVMSource',
4244
OVA: 'OvaSource',
4345
};
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
<script>
2+
import CruResource from '@shell/components/CruResource';
3+
import Tabbed from '@shell/components/Tabbed';
4+
import Tab from '@shell/components/Tabbed/Tab';
5+
import { LabeledInput } from '@components/Form/LabeledInput';
6+
import LabeledSelect from '@shell/components/form/LabeledSelect';
7+
import NameNsDescription from '@shell/components/form/NameNsDescription';
8+
import { RadioGroup } from '@components/Form/Radio';
9+
import CreateEditView from '@shell/mixins/create-edit-view';
10+
import FormValidation from '@shell/mixins/form-validation';
11+
import { SECRET } from '@shell/config/types';
12+
import { randomStr } from '@shell/utils/string';
13+
import { mapGetters } from 'vuex';
14+
15+
export default {
16+
name: 'EditKVMSource',
17+
18+
emits: ['update:value'],
19+
20+
components: {
21+
CruResource,
22+
Tabbed,
23+
Tab,
24+
LabeledInput,
25+
LabeledSelect,
26+
NameNsDescription,
27+
RadioGroup,
28+
},
29+
30+
mixins: [CreateEditView, FormValidation],
31+
32+
inheritAttrs: false,
33+
34+
props: {
35+
value: {
36+
type: Object,
37+
required: true,
38+
},
39+
mode: {
40+
type: String,
41+
required: true,
42+
},
43+
},
44+
45+
async fetch() {
46+
const inStore = this.$store.getters['currentProduct'].inStore;
47+
48+
this.allSecrets = await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
49+
},
50+
51+
data() {
52+
if (!this.value.spec) this.value.spec = {};
53+
if (!this.value.spec.credentials) this.value.spec.credentials = {};
54+
55+
const initialMode = this.value.spec.credentials.name ? 'existing' : 'new';
56+
57+
return {
58+
allSecrets: [],
59+
authMode: initialMode,
60+
newUsername: '',
61+
newPassword: '',
62+
newPrivateKey: '',
63+
64+
fvFormRuleSets: [
65+
{ path: 'metadata.name', rules: ['nameRequired'] },
66+
{ path: 'spec.libvirtURI', rules: ['uriRequired'] },
67+
],
68+
};
69+
},
70+
71+
computed: {
72+
...mapGetters({ t: 'i18n/t' }),
73+
74+
authModeOptions() {
75+
return [
76+
{ label: this.t('harvester.addons.vmImport.fields.createSecret'), value: 'new' },
77+
{ label: this.t('harvester.addons.vmImport.fields.useSecret'), value: 'existing' }
78+
];
79+
},
80+
81+
secretOptions() {
82+
const currentNamespace = this.value.metadata.namespace || 'default';
83+
84+
return this.allSecrets
85+
.filter((s) => s.metadata.namespace === currentNamespace)
86+
.map((s) => ({
87+
label: s.nameDisplay,
88+
value: s.metadata.name
89+
}));
90+
},
91+
92+
fvExtraRules() {
93+
return {
94+
nameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.fields.name') }) : undefined,
95+
uriRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.kvm.fields.uri') }) : undefined,
96+
};
97+
},
98+
99+
isFormValid() {
100+
if (!this.fvFormIsValid) {
101+
return false;
102+
}
103+
104+
if (this.authMode === 'new') {
105+
if (!this.newUsername || !this.newPassword) return false;
106+
} else {
107+
if (!this.value.spec.credentials.name) return false;
108+
}
109+
110+
return true;
111+
}
112+
},
113+
114+
methods: {
115+
usernameRule(val) {
116+
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.username') }) : undefined;
117+
},
118+
secretRule(val) {
119+
return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.selectSecret') }) : undefined;
120+
},
121+
122+
async saveSource(buttonCb) {
123+
const inStore = this.$store.getters['currentProduct'].inStore;
124+
125+
try {
126+
if (this.authMode === 'new') {
127+
const secretName = `${ this.value.metadata.name }-creds-${ randomStr(4).toLowerCase() }`;
128+
const namespace = this.value.metadata.namespace || 'default';
129+
130+
// Create the model with the correct Schema ID (SECRET)
131+
const newSecret = await this.$store.dispatch(`${ inStore }/create`, {
132+
type: SECRET,
133+
metadata: {
134+
name: secretName,
135+
namespace
136+
}
137+
});
138+
139+
// Use '_type' to set the Kubernetes 'type' field.
140+
newSecret['_type'] = 'Opaque';
141+
142+
// base64 encode the data
143+
newSecret['data'] = {
144+
username: btoa(this.newUsername),
145+
password: btoa(this.newPassword),
146+
privateKey: this.newPrivateKey ? btoa(this.newPrivateKey) : undefined
147+
};
148+
149+
await newSecret.save();
150+
151+
this.value.spec.credentials = {
152+
name: secretName,
153+
namespace
154+
};
155+
}
156+
157+
await this.save(buttonCb);
158+
} catch (err) {
159+
this.errors = [err];
160+
buttonCb(false);
161+
}
162+
}
163+
}
164+
};
165+
</script>
166+
167+
<template>
168+
<CruResource
169+
:done-route="doneRoute"
170+
:resource="value"
171+
:mode="mode"
172+
:errors="errors"
173+
:apply-hooks="applyHooks"
174+
:validation-passed="isFormValid"
175+
@finish="saveSource"
176+
@error="e=>errors=e"
177+
>
178+
<NameNsDescription
179+
:value="value"
180+
:mode="mode"
181+
:rules="{ name: fvGetAndReportPathRules('metadata.name') }"
182+
@update:value="$emit('update:value', $event)"
183+
/>
184+
185+
<Tabbed
186+
v-bind="$attrs"
187+
class="mt-15"
188+
:side-tabs="true"
189+
>
190+
<Tab
191+
name="basic"
192+
:label="t('harvester.addons.vmImport.titles.basic')"
193+
:weight="2"
194+
>
195+
<div class="row mb-20">
196+
<div class="col span-12">
197+
<LabeledInput
198+
v-model:value="value.spec.libvirtURI"
199+
:label="t('harvester.addons.vmImport.kvm.fields.uri')"
200+
:placeholder="t('harvester.addons.vmImport.kvm.placeholders.uri')"
201+
:mode="mode"
202+
:rules="fvGetAndReportPathRules('spec.libvirtURI')"
203+
required
204+
/>
205+
</div>
206+
</div>
207+
</Tab>
208+
209+
<Tab
210+
name="auth"
211+
:label="t('harvester.addons.vmImport.titles.auth')"
212+
:weight="1"
213+
>
214+
<div class="row mb-20">
215+
<div class="col span-12">
216+
<RadioGroup
217+
v-model:value="authMode"
218+
name="authMode"
219+
:options="authModeOptions"
220+
:mode="mode"
221+
/>
222+
</div>
223+
</div>
224+
225+
<div v-if="authMode === 'new'">
226+
<div class="row mb-20">
227+
<div class="col span-6">
228+
<LabeledInput
229+
v-model:value="newUsername"
230+
:label="t('harvester.addons.vmImport.fields.username')"
231+
:mode="mode"
232+
:rules="[usernameRule]"
233+
required
234+
/>
235+
</div>
236+
<div class="col span-6">
237+
<LabeledInput
238+
v-model:value="newPassword"
239+
type="password"
240+
:label="t('harvester.addons.vmImport.fields.password')"
241+
placeholder="(Optional)"
242+
:mode="mode"
243+
/>
244+
</div>
245+
</div>
246+
<div class="row mb-20">
247+
<div class="col span-12">
248+
<LabeledInput
249+
v-model:value="newPrivateKey"
250+
type="multiline"
251+
:label="t('harvester.addons.vmImport.kvm.fields.privateKey')"
252+
:placeholder="t('harvester.addons.vmImport.kvm.placeholders.privateKey')"
253+
:min-height="100"
254+
:mode="mode"
255+
/>
256+
</div>
257+
</div>
258+
259+
<div class="text-muted">
260+
Note: A new Kubernetes Secret will be created to store these credentials.
261+
</div>
262+
</div>
263+
264+
<div v-if="authMode === 'existing'">
265+
<div class="row mb-20">
266+
<div class="col span-6">
267+
<LabeledSelect
268+
v-model:value="value.spec.credentials.name"
269+
:options="secretOptions"
270+
:label="t('harvester.addons.vmImport.fields.selectSecret')"
271+
:mode="mode"
272+
:rules="[secretRule]"
273+
required
274+
/>
275+
</div>
276+
</div>
277+
</div>
278+
</Tab>
279+
</Tabbed>
280+
</CruResource>
281+
</template>

0 commit comments

Comments
 (0)