]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | import { Component, OnInit } from '@angular/core'; |
2 | import { FormArray, FormControl, Validators } from '@angular/forms'; | |
3 | import { ActivatedRoute, Router } from '@angular/router'; | |
4 | ||
5 | import { I18n } from '@ngx-translate/i18n-polyfill'; | |
6 | import * as _ from 'lodash'; | |
7 | import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; | |
8 | import { forkJoin } from 'rxjs'; | |
9 | ||
10 | import { IscsiService } from '../../../shared/api/iscsi.service'; | |
11 | import { RbdService } from '../../../shared/api/rbd.service'; | |
12 | import { SelectMessages } from '../../../shared/components/select/select-messages.model'; | |
13 | import { SelectOption } from '../../../shared/components/select/select-option.model'; | |
14 | import { ActionLabelsI18n } from '../../../shared/constants/app.constants'; | |
15 | import { CdFormGroup } from '../../../shared/forms/cd-form-group'; | |
16 | import { CdValidators } from '../../../shared/forms/cd-validators'; | |
17 | import { FinishedTask } from '../../../shared/models/finished-task'; | |
18 | import { TaskWrapperService } from '../../../shared/services/task-wrapper.service'; | |
19 | import { IscsiTargetImageSettingsModalComponent } from '../iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component'; | |
20 | import { IscsiTargetIqnSettingsModalComponent } from '../iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component'; | |
21 | ||
22 | @Component({ | |
23 | selector: 'cd-iscsi-target-form', | |
24 | templateUrl: './iscsi-target-form.component.html', | |
25 | styleUrls: ['./iscsi-target-form.component.scss'] | |
26 | }) | |
27 | export class IscsiTargetFormComponent implements OnInit { | |
eafe8130 | 28 | cephIscsiConfigVersion: number; |
11fdf7f2 TL |
29 | targetForm: CdFormGroup; |
30 | modalRef: BsModalRef; | |
eafe8130 | 31 | api_version = 0; |
11fdf7f2 TL |
32 | minimum_gateways = 1; |
33 | target_default_controls: any; | |
eafe8130 | 34 | target_controls_limits: any; |
11fdf7f2 | 35 | disk_default_controls: any; |
eafe8130 | 36 | disk_controls_limits: any; |
11fdf7f2 TL |
37 | backstores: string[]; |
38 | default_backstore: string; | |
81eedcae | 39 | unsupported_rbd_features: any; |
11fdf7f2 TL |
40 | required_rbd_features: any; |
41 | ||
42 | isEdit = false; | |
43 | target_iqn: string; | |
44 | ||
45 | imagesAll: any[]; | |
46 | imagesSelections: SelectOption[]; | |
47 | portalsSelections: SelectOption[] = []; | |
48 | ||
49 | imagesInitiatorSelections: SelectOption[][] = []; | |
50 | groupDiskSelections: SelectOption[][] = []; | |
51 | groupMembersSelections: SelectOption[][] = []; | |
52 | ||
53 | imagesSettings: any = {}; | |
54 | messages = { | |
55 | portals: new SelectMessages( | |
56 | { noOptions: this.i18n('There are no portals available.') }, | |
57 | this.i18n | |
58 | ), | |
59 | images: new SelectMessages( | |
60 | { noOptions: this.i18n('There are no images available.') }, | |
61 | this.i18n | |
62 | ), | |
63 | initiatorImage: new SelectMessages( | |
64 | { | |
65 | noOptions: this.i18n( | |
66 | 'There are no images available. Please make sure you add an image to the target.' | |
67 | ) | |
68 | }, | |
69 | this.i18n | |
70 | ), | |
71 | groupInitiator: new SelectMessages( | |
72 | { | |
73 | noOptions: this.i18n( | |
74 | 'There are no initiators available. Please make sure you add an initiator to the target.' | |
75 | ) | |
76 | }, | |
77 | this.i18n | |
78 | ) | |
79 | }; | |
80 | ||
81 | IQN_REGEX = /^iqn\.(19|20)\d\d-(0[1-9]|1[0-2])\.\D{2,3}(\.[A-Za-z0-9-]+)+(:[A-Za-z0-9-\.]+)*$/; | |
82 | USER_REGEX = /[\w\.:@_-]{8,64}/; | |
83 | PASSWORD_REGEX = /[\w@\-_\/]{12,16}/; | |
84 | action: string; | |
85 | resource: string; | |
86 | ||
87 | constructor( | |
88 | private iscsiService: IscsiService, | |
89 | private modalService: BsModalService, | |
90 | private rbdService: RbdService, | |
91 | private router: Router, | |
92 | private route: ActivatedRoute, | |
93 | private i18n: I18n, | |
94 | private taskWrapper: TaskWrapperService, | |
95 | public actionLabels: ActionLabelsI18n | |
96 | ) { | |
97 | this.resource = this.i18n('target'); | |
98 | } | |
99 | ||
100 | ngOnInit() { | |
101 | const promises: any[] = [ | |
102 | this.iscsiService.listTargets(), | |
103 | this.rbdService.list(), | |
104 | this.iscsiService.portals(), | |
eafe8130 TL |
105 | this.iscsiService.settings(), |
106 | this.iscsiService.version() | |
11fdf7f2 TL |
107 | ]; |
108 | ||
109 | if (this.router.url.startsWith('/block/iscsi/targets/edit')) { | |
110 | this.isEdit = true; | |
111 | this.route.params.subscribe((params: { target_iqn: string }) => { | |
112 | this.target_iqn = decodeURIComponent(params.target_iqn); | |
113 | promises.push(this.iscsiService.getTarget(this.target_iqn)); | |
114 | }); | |
115 | } | |
116 | this.action = this.isEdit ? this.actionLabels.EDIT : this.actionLabels.CREATE; | |
117 | ||
118 | forkJoin(promises).subscribe((data: any[]) => { | |
119 | // iscsiService.listTargets | |
120 | const usedImages = _(data[0]) | |
121 | .filter((target) => target.target_iqn !== this.target_iqn) | |
122 | .flatMap((target) => target.disks) | |
123 | .map((image) => `${image.pool}/${image.image}`) | |
124 | .value(); | |
125 | ||
126 | // iscsiService.settings() | |
eafe8130 TL |
127 | if ('api_version' in data[3]) { |
128 | this.api_version = data[3].api_version; | |
129 | } | |
11fdf7f2 TL |
130 | this.minimum_gateways = data[3].config.minimum_gateways; |
131 | this.target_default_controls = data[3].target_default_controls; | |
eafe8130 | 132 | this.target_controls_limits = data[3].target_controls_limits; |
11fdf7f2 | 133 | this.disk_default_controls = data[3].disk_default_controls; |
eafe8130 | 134 | this.disk_controls_limits = data[3].disk_controls_limits; |
11fdf7f2 TL |
135 | this.backstores = data[3].backstores; |
136 | this.default_backstore = data[3].default_backstore; | |
81eedcae | 137 | this.unsupported_rbd_features = data[3].unsupported_rbd_features; |
11fdf7f2 TL |
138 | this.required_rbd_features = data[3].required_rbd_features; |
139 | ||
140 | // rbdService.list() | |
141 | this.imagesAll = _(data[1]) | |
142 | .flatMap((pool) => pool.value) | |
143 | .filter((image) => { | |
144 | const imageId = `${image.pool_name}/${image.name}`; | |
145 | if (usedImages.indexOf(imageId) !== -1) { | |
146 | return false; | |
147 | } | |
148 | const validBackstores = this.getValidBackstores(image); | |
149 | if (validBackstores.length === 0) { | |
150 | return false; | |
151 | } | |
152 | return true; | |
153 | }) | |
154 | .value(); | |
155 | ||
156 | this.imagesSelections = this.imagesAll.map( | |
157 | (image) => new SelectOption(false, `${image.pool_name}/${image.name}`, '') | |
158 | ); | |
159 | ||
160 | // iscsiService.portals() | |
161 | const portals: SelectOption[] = []; | |
162 | data[2].forEach((portal) => { | |
163 | portal.ip_addresses.forEach((ip) => { | |
164 | portals.push(new SelectOption(false, portal.name + ':' + ip, '')); | |
165 | }); | |
166 | }); | |
167 | this.portalsSelections = [...portals]; | |
168 | ||
eafe8130 TL |
169 | // iscsiService.version() |
170 | this.cephIscsiConfigVersion = data[4]['ceph_iscsi_config_version']; | |
171 | ||
11fdf7f2 TL |
172 | this.createForm(); |
173 | ||
174 | // iscsiService.getTarget() | |
eafe8130 TL |
175 | if (data[5]) { |
176 | this.resolveModel(data[5]); | |
11fdf7f2 TL |
177 | } |
178 | }); | |
179 | } | |
180 | ||
181 | createForm() { | |
182 | this.targetForm = new CdFormGroup({ | |
183 | target_iqn: new FormControl('iqn.2001-07.com.ceph:' + Date.now(), { | |
184 | validators: [Validators.required, Validators.pattern(this.IQN_REGEX)] | |
185 | }), | |
186 | target_controls: new FormControl({}), | |
187 | portals: new FormControl([], { | |
188 | validators: [ | |
189 | CdValidators.custom('minGateways', (value) => { | |
190 | const gateways = _.uniq(value.map((elem) => elem.split(':')[0])); | |
191 | return gateways.length < Math.max(1, this.minimum_gateways); | |
192 | }) | |
193 | ] | |
194 | }), | |
eafe8130 TL |
195 | disks: new FormControl([], { |
196 | validators: [ | |
197 | CdValidators.custom('dupLunId', (value) => { | |
198 | const lunIds = this.getLunIds(value); | |
199 | return lunIds.length !== _.uniq(lunIds).length; | |
200 | }), | |
201 | CdValidators.custom('dupWwn', (value) => { | |
202 | const wwns = this.getWwns(value); | |
203 | return wwns.length !== _.uniq(wwns).length; | |
204 | }) | |
205 | ] | |
206 | }), | |
11fdf7f2 TL |
207 | initiators: new FormArray([]), |
208 | groups: new FormArray([]), | |
209 | acl_enabled: new FormControl(false) | |
210 | }); | |
eafe8130 TL |
211 | // Target level authentication was introduced in ceph-iscsi config v11 |
212 | if (this.cephIscsiConfigVersion > 10) { | |
213 | const authFormGroup = new CdFormGroup({ | |
214 | user: new FormControl(''), | |
215 | password: new FormControl(''), | |
216 | mutual_user: new FormControl(''), | |
217 | mutual_password: new FormControl('') | |
218 | }); | |
219 | this.setAuthValidator(authFormGroup); | |
220 | this.targetForm.addControl('auth', authFormGroup); | |
221 | } | |
11fdf7f2 TL |
222 | } |
223 | ||
224 | resolveModel(res) { | |
225 | this.targetForm.patchValue({ | |
226 | target_iqn: res.target_iqn, | |
227 | target_controls: res.target_controls, | |
228 | acl_enabled: res.acl_enabled | |
229 | }); | |
eafe8130 TL |
230 | // Target level authentication was introduced in ceph-iscsi config v11 |
231 | if (this.cephIscsiConfigVersion > 10) { | |
232 | this.targetForm.patchValue({ | |
233 | auth: res.auth | |
234 | }); | |
235 | } | |
11fdf7f2 TL |
236 | const portals = []; |
237 | _.forEach(res.portals, (portal) => { | |
238 | const id = `${portal.host}:${portal.ip}`; | |
239 | portals.push(id); | |
240 | }); | |
241 | this.targetForm.patchValue({ | |
242 | portals: portals | |
243 | }); | |
244 | ||
245 | const disks = []; | |
246 | _.forEach(res.disks, (disk) => { | |
247 | const id = `${disk.pool}/${disk.image}`; | |
248 | disks.push(id); | |
249 | this.imagesSettings[id] = { | |
250 | backstore: disk.backstore | |
251 | }; | |
252 | this.imagesSettings[id][disk.backstore] = disk.controls; | |
eafe8130 TL |
253 | if ('lun' in disk) { |
254 | this.imagesSettings[id]['lun'] = disk.lun; | |
255 | } | |
256 | if ('wwn' in disk) { | |
257 | this.imagesSettings[id]['wwn'] = disk.wwn; | |
258 | } | |
11fdf7f2 TL |
259 | |
260 | this.onImageSelection({ option: { name: id, selected: true } }); | |
261 | }); | |
262 | this.targetForm.patchValue({ | |
263 | disks: disks | |
264 | }); | |
265 | ||
266 | _.forEach(res.clients, (client) => { | |
267 | const initiator = this.addInitiator(); | |
268 | client.luns = _.map(client.luns, (lun) => `${lun.pool}/${lun.image}`); | |
269 | initiator.patchValue(client); | |
270 | // updatedInitiatorSelector() | |
271 | }); | |
272 | ||
273 | _.forEach(res.groups, (group) => { | |
274 | const fg = this.addGroup(); | |
11fdf7f2 TL |
275 | group.disks = _.map(group.disks, (disk) => `${disk.pool}/${disk.image}`); |
276 | fg.patchValue(group); | |
277 | _.forEach(group.members, (member) => { | |
278 | this.onGroupMemberSelection({ option: new SelectOption(true, member, '') }); | |
279 | }); | |
280 | }); | |
281 | } | |
282 | ||
283 | hasAdvancedSettings(settings: any) { | |
284 | return Object.values(settings).length > 0; | |
285 | } | |
286 | ||
287 | // Portals | |
288 | get portals() { | |
289 | return this.targetForm.get('portals') as FormControl; | |
290 | } | |
291 | ||
292 | onPortalSelection() { | |
293 | this.portals.setValue(this.portals.value); | |
294 | } | |
295 | ||
296 | removePortal(index: number, portal: string) { | |
297 | this.portalsSelections.forEach((value) => { | |
298 | if (value.name === portal) { | |
299 | value.selected = false; | |
300 | } | |
301 | }); | |
302 | ||
303 | this.portals.value.splice(index, 1); | |
304 | this.portals.setValue(this.portals.value); | |
305 | return false; | |
306 | } | |
307 | ||
308 | // Images | |
309 | get disks() { | |
310 | return this.targetForm.get('disks') as FormControl; | |
311 | } | |
312 | ||
313 | removeImage(index: number, image: string) { | |
314 | this.imagesSelections.forEach((value) => { | |
315 | if (value.name === image) { | |
316 | value.selected = false; | |
317 | } | |
318 | }); | |
319 | this.disks.value.splice(index, 1); | |
320 | this.removeImageRefs(image); | |
eafe8130 | 321 | this.targetForm.get('disks').updateValueAndValidity({ emitEvent: false }); |
11fdf7f2 TL |
322 | return false; |
323 | } | |
324 | ||
325 | removeImageRefs(name) { | |
326 | this.initiators.controls.forEach((element) => { | |
327 | const newImages = element.value.luns.filter((item) => item !== name); | |
328 | element.get('luns').setValue(newImages); | |
329 | }); | |
330 | ||
331 | this.groups.controls.forEach((element) => { | |
332 | const newDisks = element.value.disks.filter((item) => item !== name); | |
333 | element.get('disks').setValue(newDisks); | |
334 | }); | |
335 | ||
336 | _.forEach(this.imagesInitiatorSelections, (selections, i) => { | |
337 | this.imagesInitiatorSelections[i] = selections.filter((item: any) => item.name !== name); | |
338 | }); | |
339 | _.forEach(this.groupDiskSelections, (selections, i) => { | |
340 | this.groupDiskSelections[i] = selections.filter((item: any) => item.name !== name); | |
341 | }); | |
342 | } | |
343 | ||
344 | getDefaultBackstore(imageId) { | |
345 | let result = this.default_backstore; | |
346 | const image = this.getImageById(imageId); | |
347 | if (!this.validFeatures(image, this.default_backstore)) { | |
348 | this.backstores.forEach((backstore) => { | |
349 | if (backstore !== this.default_backstore) { | |
350 | if (this.validFeatures(image, backstore)) { | |
351 | result = backstore; | |
352 | } | |
353 | } | |
354 | }); | |
355 | } | |
356 | return result; | |
357 | } | |
358 | ||
eafe8130 TL |
359 | isLunIdInUse(lunId, imageId) { |
360 | const images = this.disks.value.filter((currentImageId) => currentImageId !== imageId); | |
361 | return this.getLunIds(images).includes(lunId); | |
362 | } | |
363 | ||
364 | getLunIds(images) { | |
365 | return _.map(images, (image) => this.imagesSettings[image]['lun']); | |
366 | } | |
367 | ||
368 | nextLunId(imageId) { | |
369 | const images = this.disks.value.filter((currentImageId) => currentImageId !== imageId); | |
370 | const lunIdsInUse = this.getLunIds(images); | |
371 | let lunIdCandidate = 0; | |
372 | while (lunIdsInUse.includes(lunIdCandidate)) { | |
373 | lunIdCandidate++; | |
374 | } | |
375 | return lunIdCandidate; | |
376 | } | |
377 | ||
378 | getWwns(images) { | |
379 | const wwns = _.map(images, (image) => this.imagesSettings[image]['wwn']); | |
380 | return wwns.filter((wwn) => _.isString(wwn) && wwn !== ''); | |
381 | } | |
382 | ||
11fdf7f2 TL |
383 | onImageSelection($event) { |
384 | const option = $event.option; | |
385 | ||
386 | if (option.selected) { | |
387 | if (!this.imagesSettings[option.name]) { | |
388 | const defaultBackstore = this.getDefaultBackstore(option.name); | |
389 | this.imagesSettings[option.name] = { | |
eafe8130 TL |
390 | backstore: defaultBackstore, |
391 | lun: this.nextLunId(option.name) | |
11fdf7f2 TL |
392 | }; |
393 | this.imagesSettings[option.name][defaultBackstore] = {}; | |
eafe8130 TL |
394 | } else if (this.isLunIdInUse(this.imagesSettings[option.name]['lun'], option.name)) { |
395 | // If the lun id is now in use, we have to generate a new one | |
396 | this.imagesSettings[option.name]['lun'] = this.nextLunId(option.name); | |
11fdf7f2 TL |
397 | } |
398 | ||
399 | _.forEach(this.imagesInitiatorSelections, (selections, i) => { | |
400 | selections.push(new SelectOption(false, option.name, '')); | |
401 | this.imagesInitiatorSelections[i] = [...selections]; | |
402 | }); | |
403 | ||
404 | _.forEach(this.groupDiskSelections, (selections, i) => { | |
405 | selections.push(new SelectOption(false, option.name, '')); | |
406 | this.groupDiskSelections[i] = [...selections]; | |
407 | }); | |
408 | } else { | |
409 | this.removeImageRefs(option.name); | |
410 | } | |
eafe8130 | 411 | this.targetForm.get('disks').updateValueAndValidity({ emitEvent: false }); |
11fdf7f2 TL |
412 | } |
413 | ||
414 | // Initiators | |
415 | get initiators() { | |
416 | return this.targetForm.get('initiators') as FormArray; | |
417 | } | |
418 | ||
419 | addInitiator() { | |
420 | const fg = new CdFormGroup({ | |
421 | client_iqn: new FormControl('', { | |
422 | validators: [ | |
423 | Validators.required, | |
424 | CdValidators.custom('notUnique', (client_iqn) => { | |
425 | const flattened = this.initiators.controls.reduce(function(accumulator, currentValue) { | |
426 | return accumulator.concat(currentValue.value.client_iqn); | |
427 | }, []); | |
428 | ||
429 | return flattened.indexOf(client_iqn) !== flattened.lastIndexOf(client_iqn); | |
430 | }), | |
431 | Validators.pattern(this.IQN_REGEX) | |
432 | ] | |
433 | }), | |
434 | auth: new CdFormGroup({ | |
435 | user: new FormControl(''), | |
436 | password: new FormControl(''), | |
437 | mutual_user: new FormControl(''), | |
438 | mutual_password: new FormControl('') | |
439 | }), | |
440 | luns: new FormControl([]), | |
441 | cdIsInGroup: new FormControl(false) | |
442 | }); | |
443 | ||
eafe8130 TL |
444 | this.setAuthValidator(fg); |
445 | ||
446 | this.initiators.push(fg); | |
447 | ||
448 | _.forEach(this.groupMembersSelections, (selections, i) => { | |
449 | selections.push(new SelectOption(false, '', '')); | |
450 | this.groupMembersSelections[i] = [...selections]; | |
451 | }); | |
452 | ||
453 | const disks = _.map( | |
454 | this.targetForm.getValue('disks'), | |
455 | (disk) => new SelectOption(false, disk, '') | |
456 | ); | |
457 | this.imagesInitiatorSelections.push(disks); | |
458 | ||
459 | return fg; | |
460 | } | |
461 | ||
462 | setAuthValidator(fg: CdFormGroup) { | |
11fdf7f2 TL |
463 | CdValidators.validateIf( |
464 | fg.get('user'), | |
465 | () => fg.getValue('password') || fg.getValue('mutual_user') || fg.getValue('mutual_password'), | |
466 | [Validators.required], | |
467 | [Validators.pattern(this.USER_REGEX)], | |
468 | [fg.get('password'), fg.get('mutual_user'), fg.get('mutual_password')] | |
469 | ); | |
470 | ||
471 | CdValidators.validateIf( | |
472 | fg.get('password'), | |
473 | () => fg.getValue('user') || fg.getValue('mutual_user') || fg.getValue('mutual_password'), | |
474 | [Validators.required], | |
475 | [Validators.pattern(this.PASSWORD_REGEX)], | |
476 | [fg.get('user'), fg.get('mutual_user'), fg.get('mutual_password')] | |
477 | ); | |
478 | ||
479 | CdValidators.validateIf( | |
480 | fg.get('mutual_user'), | |
481 | () => fg.getValue('mutual_password'), | |
482 | [Validators.required], | |
483 | [Validators.pattern(this.USER_REGEX)], | |
484 | [fg.get('user'), fg.get('password'), fg.get('mutual_password')] | |
485 | ); | |
486 | ||
487 | CdValidators.validateIf( | |
488 | fg.get('mutual_password'), | |
489 | () => fg.getValue('mutual_user'), | |
490 | [Validators.required], | |
491 | [Validators.pattern(this.PASSWORD_REGEX)], | |
492 | [fg.get('user'), fg.get('password'), fg.get('mutual_user')] | |
493 | ); | |
11fdf7f2 TL |
494 | } |
495 | ||
496 | removeInitiator(index) { | |
497 | const removed = this.initiators.value[index]; | |
498 | ||
499 | this.initiators.removeAt(index); | |
500 | ||
501 | _.forEach(this.groupMembersSelections, (selections, i) => { | |
502 | selections.splice(index, 1); | |
503 | this.groupMembersSelections[i] = [...selections]; | |
504 | }); | |
505 | ||
506 | this.groups.controls.forEach((element) => { | |
507 | const newMembers = element.value.members.filter((item) => item !== removed.client_iqn); | |
508 | element.get('members').setValue(newMembers); | |
509 | }); | |
510 | ||
511 | this.imagesInitiatorSelections.splice(index, 1); | |
512 | } | |
513 | ||
514 | updatedInitiatorSelector() { | |
515 | // Validate all client_iqn | |
516 | this.initiators.controls.forEach((control) => { | |
517 | control.get('client_iqn').updateValueAndValidity({ emitEvent: false }); | |
518 | }); | |
519 | ||
520 | // Update Group Initiator Selector | |
521 | _.forEach(this.groupMembersSelections, (group, group_index) => { | |
522 | _.forEach(group, (elem, index) => { | |
523 | const oldName = elem.name; | |
524 | elem.name = this.initiators.controls[index].value.client_iqn; | |
525 | ||
526 | this.groups.controls.forEach((element) => { | |
527 | const members = element.value.members; | |
528 | const i = members.indexOf(oldName); | |
529 | ||
530 | if (i !== -1) { | |
531 | members[i] = elem.name; | |
532 | } | |
533 | element.get('members').setValue(members); | |
534 | }); | |
535 | }); | |
536 | this.groupMembersSelections[group_index] = [...this.groupMembersSelections[group_index]]; | |
537 | }); | |
538 | } | |
539 | ||
540 | removeInitiatorImage(initiator: any, lun_index: number, initiator_index: string, image: string) { | |
541 | const luns = initiator.getValue('luns'); | |
542 | luns.splice(lun_index, 1); | |
543 | initiator.patchValue({ luns: luns }); | |
544 | ||
545 | this.imagesInitiatorSelections[initiator_index].forEach((value) => { | |
546 | if (value.name === image) { | |
547 | value.selected = false; | |
548 | } | |
549 | }); | |
550 | ||
551 | return false; | |
552 | } | |
553 | ||
554 | // Groups | |
555 | get groups() { | |
556 | return this.targetForm.get('groups') as FormArray; | |
557 | } | |
558 | ||
559 | addGroup() { | |
560 | const fg = new CdFormGroup({ | |
561 | group_id: new FormControl('', { validators: [Validators.required] }), | |
562 | members: new FormControl([]), | |
563 | disks: new FormControl([]) | |
564 | }); | |
565 | ||
566 | this.groups.push(fg); | |
567 | ||
568 | const disks = _.map( | |
569 | this.targetForm.getValue('disks'), | |
570 | (disk) => new SelectOption(false, disk, '') | |
571 | ); | |
572 | this.groupDiskSelections.push(disks); | |
573 | ||
574 | const initiators = _.map( | |
575 | this.initiators.value, | |
81eedcae | 576 | (initiator) => new SelectOption(false, initiator.client_iqn, '', !initiator.cdIsInGroup) |
11fdf7f2 TL |
577 | ); |
578 | this.groupMembersSelections.push(initiators); | |
579 | ||
580 | return fg; | |
581 | } | |
582 | ||
583 | removeGroup(index) { | |
584 | this.groups.removeAt(index); | |
585 | this.groupDiskSelections.splice(index, 1); | |
586 | } | |
587 | ||
588 | onGroupMemberSelection($event) { | |
589 | const option = $event.option; | |
590 | ||
81eedcae TL |
591 | let initiator_index: number; |
592 | this.initiators.controls.forEach((element, index) => { | |
11fdf7f2 TL |
593 | if (element.value.client_iqn === option.name) { |
594 | element.patchValue({ luns: [] }); | |
595 | element.get('cdIsInGroup').setValue(option.selected); | |
81eedcae | 596 | initiator_index = index; |
11fdf7f2 TL |
597 | } |
598 | }); | |
81eedcae TL |
599 | |
600 | // Members can only be at one group at a time, so when a member is selected | |
601 | // in one group we need to disable its selection in other groups | |
602 | _.forEach(this.groupMembersSelections, (group) => { | |
603 | group[initiator_index].enabled = !option.selected; | |
604 | }); | |
11fdf7f2 TL |
605 | } |
606 | ||
607 | removeGroupInitiator(group, member_index, group_index) { | |
608 | const name = group.getValue('members')[member_index]; | |
609 | group.getValue('members').splice(member_index, 1); | |
610 | ||
611 | this.groupMembersSelections[group_index].forEach((value) => { | |
612 | if (value.name === name) { | |
613 | value.selected = false; | |
614 | } | |
615 | }); | |
616 | this.groupMembersSelections[group_index] = [...this.groupMembersSelections[group_index]]; | |
617 | ||
618 | this.onGroupMemberSelection({ option: new SelectOption(false, name, '') }); | |
619 | } | |
620 | ||
621 | removeGroupDisk(group, disk_index, group_index) { | |
622 | const name = group.getValue('disks')[disk_index]; | |
623 | group.getValue('disks').splice(disk_index, 1); | |
624 | ||
625 | this.groupDiskSelections[group_index].forEach((value) => { | |
626 | if (value.name === name) { | |
627 | value.selected = false; | |
628 | } | |
629 | }); | |
630 | this.groupDiskSelections[group_index] = [...this.groupDiskSelections[group_index]]; | |
631 | } | |
632 | ||
633 | submit() { | |
634 | const formValue = _.cloneDeep(this.targetForm.value); | |
635 | ||
636 | const request = { | |
637 | target_iqn: this.targetForm.getValue('target_iqn'), | |
638 | target_controls: this.targetForm.getValue('target_controls'), | |
639 | acl_enabled: this.targetForm.getValue('acl_enabled'), | |
640 | portals: [], | |
641 | disks: [], | |
642 | clients: [], | |
643 | groups: [] | |
644 | }; | |
645 | ||
eafe8130 TL |
646 | // Target level authentication was introduced in ceph-iscsi config v11 |
647 | if (this.cephIscsiConfigVersion > 10) { | |
648 | const targetAuth: CdFormGroup = this.targetForm.get('auth') as CdFormGroup; | |
649 | if (!targetAuth.getValue('user')) { | |
650 | targetAuth.get('user').setValue(''); | |
651 | } | |
652 | if (!targetAuth.getValue('password')) { | |
653 | targetAuth.get('password').setValue(''); | |
654 | } | |
655 | if (!targetAuth.getValue('mutual_user')) { | |
656 | targetAuth.get('mutual_user').setValue(''); | |
657 | } | |
658 | if (!targetAuth.getValue('mutual_password')) { | |
659 | targetAuth.get('mutual_password').setValue(''); | |
660 | } | |
661 | const acl_enabled = this.targetForm.getValue('acl_enabled'); | |
662 | request['auth'] = { | |
663 | user: acl_enabled ? '' : targetAuth.getValue('user'), | |
664 | password: acl_enabled ? '' : targetAuth.getValue('password'), | |
665 | mutual_user: acl_enabled ? '' : targetAuth.getValue('mutual_user'), | |
666 | mutual_password: acl_enabled ? '' : targetAuth.getValue('mutual_password') | |
667 | }; | |
668 | } | |
669 | ||
11fdf7f2 TL |
670 | // Disks |
671 | formValue.disks.forEach((disk) => { | |
672 | const imageSplit = disk.split('/'); | |
673 | const backstore = this.imagesSettings[disk].backstore; | |
674 | request.disks.push({ | |
675 | pool: imageSplit[0], | |
676 | image: imageSplit[1], | |
677 | backstore: backstore, | |
eafe8130 TL |
678 | controls: this.imagesSettings[disk][backstore], |
679 | lun: this.imagesSettings[disk]['lun'], | |
680 | wwn: this.imagesSettings[disk]['wwn'] | |
11fdf7f2 TL |
681 | }); |
682 | }); | |
683 | ||
684 | // Portals | |
685 | formValue.portals.forEach((portal) => { | |
81eedcae | 686 | const index = portal.indexOf(':'); |
11fdf7f2 | 687 | request.portals.push({ |
81eedcae TL |
688 | host: portal.substring(0, index), |
689 | ip: portal.substring(index + 1) | |
11fdf7f2 TL |
690 | }); |
691 | }); | |
692 | ||
693 | // Clients | |
694 | if (request.acl_enabled) { | |
695 | formValue.initiators.forEach((initiator) => { | |
696 | if (!initiator.auth.user) { | |
697 | initiator.auth.user = ''; | |
698 | } | |
699 | if (!initiator.auth.password) { | |
700 | initiator.auth.password = ''; | |
701 | } | |
702 | if (!initiator.auth.mutual_user) { | |
703 | initiator.auth.mutual_user = ''; | |
704 | } | |
705 | if (!initiator.auth.mutual_password) { | |
706 | initiator.auth.mutual_password = ''; | |
707 | } | |
708 | delete initiator.cdIsInGroup; | |
709 | ||
710 | const newLuns = []; | |
711 | initiator.luns.forEach((lun) => { | |
712 | const imageSplit = lun.split('/'); | |
713 | newLuns.push({ | |
714 | pool: imageSplit[0], | |
715 | image: imageSplit[1] | |
716 | }); | |
717 | }); | |
718 | ||
719 | initiator.luns = newLuns; | |
720 | }); | |
721 | request.clients = formValue.initiators; | |
722 | } | |
723 | ||
724 | // Groups | |
725 | if (request.acl_enabled) { | |
726 | formValue.groups.forEach((group) => { | |
727 | const newDisks = []; | |
728 | group.disks.forEach((disk) => { | |
729 | const imageSplit = disk.split('/'); | |
730 | newDisks.push({ | |
731 | pool: imageSplit[0], | |
732 | image: imageSplit[1] | |
733 | }); | |
734 | }); | |
735 | ||
736 | group.disks = newDisks; | |
737 | }); | |
738 | request.groups = formValue.groups; | |
739 | } | |
740 | ||
741 | let wrapTask; | |
742 | if (this.isEdit) { | |
743 | request['new_target_iqn'] = request.target_iqn; | |
744 | request.target_iqn = this.target_iqn; | |
745 | wrapTask = this.taskWrapper.wrapTaskAroundCall({ | |
746 | task: new FinishedTask('iscsi/target/edit', { | |
747 | target_iqn: request.target_iqn | |
748 | }), | |
749 | call: this.iscsiService.updateTarget(this.target_iqn, request) | |
750 | }); | |
751 | } else { | |
752 | wrapTask = this.taskWrapper.wrapTaskAroundCall({ | |
753 | task: new FinishedTask('iscsi/target/create', { | |
754 | target_iqn: request.target_iqn | |
755 | }), | |
756 | call: this.iscsiService.createTarget(request) | |
757 | }); | |
758 | } | |
759 | ||
760 | wrapTask.subscribe( | |
761 | undefined, | |
762 | () => { | |
763 | this.targetForm.setErrors({ cdSubmitButton: true }); | |
764 | }, | |
765 | () => this.router.navigate(['/block/iscsi/targets']) | |
766 | ); | |
767 | } | |
768 | ||
769 | targetSettingsModal() { | |
770 | const initialState = { | |
771 | target_controls: this.targetForm.get('target_controls'), | |
eafe8130 TL |
772 | target_default_controls: this.target_default_controls, |
773 | target_controls_limits: this.target_controls_limits | |
11fdf7f2 TL |
774 | }; |
775 | ||
776 | this.modalRef = this.modalService.show(IscsiTargetIqnSettingsModalComponent, { initialState }); | |
777 | } | |
778 | ||
779 | imageSettingsModal(image) { | |
780 | const initialState = { | |
781 | imagesSettings: this.imagesSettings, | |
782 | image: image, | |
eafe8130 | 783 | api_version: this.api_version, |
11fdf7f2 | 784 | disk_default_controls: this.disk_default_controls, |
eafe8130 TL |
785 | disk_controls_limits: this.disk_controls_limits, |
786 | backstores: this.getValidBackstores(this.getImageById(image)), | |
787 | control: this.targetForm.get('disks') | |
11fdf7f2 TL |
788 | }; |
789 | ||
790 | this.modalRef = this.modalService.show(IscsiTargetImageSettingsModalComponent, { | |
791 | initialState | |
792 | }); | |
793 | } | |
794 | ||
795 | validFeatures(image, backstore) { | |
796 | const imageFeatures = image.features; | |
797 | const requiredFeatures = this.required_rbd_features[backstore]; | |
81eedcae | 798 | const unsupportedFeatures = this.unsupported_rbd_features[backstore]; |
11fdf7f2 TL |
799 | // tslint:disable-next-line:no-bitwise |
800 | const validRequiredFeatures = (imageFeatures & requiredFeatures) === requiredFeatures; | |
801 | // tslint:disable-next-line:no-bitwise | |
81eedcae | 802 | const validSupportedFeatures = (imageFeatures & unsupportedFeatures) === 0; |
11fdf7f2 TL |
803 | return validRequiredFeatures && validSupportedFeatures; |
804 | } | |
805 | ||
806 | getImageById(imageId) { | |
807 | return this.imagesAll.find((image) => imageId === `${image.pool_name}/${image.name}`); | |
808 | } | |
809 | ||
810 | getValidBackstores(image) { | |
811 | return this.backstores.filter((backstore) => this.validFeatures(image, backstore)); | |
812 | } | |
813 | } |