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