IscsiClient.instance(gateway_name=gateway_name).delete_group(target_iqn,
group_id)
TaskManager.current_task().inc_progress(task_progress_inc)
+ deleted_clients = []
for client_iqn in list(target_config['clients'].keys()):
if IscsiTarget._client_deletion_required(target, new_target_iqn, new_target_controls,
new_clients, client_iqn,
new_groups, deleted_groups):
+ deleted_clients.append(client_iqn)
IscsiClient.instance(gateway_name=gateway_name).delete_client(target_iqn,
client_iqn)
TaskManager.current_task().inc_progress(task_progress_inc)
if IscsiTarget._target_lun_deletion_required(target, new_target_iqn,
new_target_controls,
new_disks, image_id):
+ all_clients = target_config['clients'].keys()
+ not_deleted_clients = [c for c in all_clients if c not in deleted_clients]
+ for client_iqn in not_deleted_clients:
+ client_image_ids = target_config['clients'][client_iqn]['luns'].keys()
+ for client_image_id in client_image_ids:
+ if image_id == client_image_id:
+ IscsiClient.instance(gateway_name=gateway_name).delete_client_lun(
+ target_iqn, client_iqn, client_image_id)
IscsiClient.instance(gateway_name=gateway_name).delete_target_lun(target_iqn,
image_id)
pool, image = image_id.split('/', 1)
image = disk['image']
image_id = '{}/{}'.format(pool, image)
backstore = disk['backstore']
+ wwn = disk.get('wwn')
+ lun = disk.get('lun')
if image_id not in config['disks']:
IscsiClient.instance(gateway_name=gateway_name).create_disk(pool,
image,
- backstore)
+ backstore,
+ wwn)
if not target_config or image_id not in target_config['disks']:
IscsiClient.instance(gateway_name=gateway_name).create_target_lun(target_iqn,
- image_id)
+ image_id,
+ lun)
controls = disk['controls']
d_conf_controls = {}
if not target_config or client_iqn not in target_config['clients']:
IscsiClient.instance(gateway_name=gateway_name).create_client(target_iqn,
client_iqn)
- for lun in client['luns']:
- pool = lun['pool']
- image = lun['image']
- image_id = '{}/{}'.format(pool, image)
- IscsiClient.instance(gateway_name=gateway_name).create_client_lun(
- target_iqn, client_iqn, image_id)
user = client['auth']['user']
password = client['auth']['password']
m_user = client['auth']['mutual_user']
m_password = client['auth']['mutual_password']
IscsiClient.instance(gateway_name=gateway_name).create_client_auth(
target_iqn, client_iqn, user, password, m_user, m_password)
+ for lun in client['luns']:
+ pool = lun['pool']
+ image = lun['image']
+ image_id = '{}/{}'.format(pool, image)
+ if not target_config or client_iqn not in target_config['clients'] or \
+ image_id not in target_config['clients'][client_iqn]['luns']:
+ IscsiClient.instance(gateway_name=gateway_name).create_client_lun(
+ target_iqn, client_iqn, image_id)
TaskManager.current_task().inc_progress(task_progress_inc)
for group in groups:
group_id = group['group_id']
type="text"
[value]="image"
disabled />
+ <div class="input-group-addon"
+ *ngIf="api_version >= 1">lun: {{ imagesSettings[image]['lun'] }}</div>
<span class="input-group-btn">
<button class="btn btn-default"
type="button"
</span>
</ng-container>
+ <input class="form-control"
+ type="hidden"
+ id="disks"
+ name="disks"
+ formControlName="disks" />
+
+ <span class="help-block"
+ *ngIf="targetForm.showError('disks', formDir, 'dupLunId')"
+ i18n>Duplicated LUN numbers.</span>
+
<span class="help-block"
- *ngIf="targetForm.showError('disks', formDir, 'required')"
- i18n>At least 1 image is required.</span>
+ *ngIf="targetForm.showError('disks', formDir, 'dupWwn')"
+ i18n>Duplicated WWN.</span>
<div class="row">
<div class="col-md-12">
id="target_password"
name="target_password"
formControlName="password" />
-
<span class="input-group-btn">
<button type="button"
class="btn btn-default"
'backstore:2': 0
},
backstores: ['backstore:1', 'backstore:2'],
- default_backstore: 'backstore:1'
+ default_backstore: 'backstore:1',
+ api_version: 1
};
const LIST_TARGET = [
{
target_iqn: 'iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw',
portals: [{ host: 'node1', ip: '192.168.100.201' }],
- disks: [{ pool: 'rbd', image: 'disk_1', controls: {}, backstore: 'backstore:1' }],
+ disks: [
+ {
+ pool: 'rbd',
+ image: 'disk_1',
+ controls: {},
+ backstore: 'backstore:1',
+ wwn: '64af6678-9694-4367-bacc-f8eb0baa'
+ }
+ ],
clients: [
{
client_iqn: 'iqn.1994-05.com.redhat:rh7-client',
- luns: [{ pool: 'rbd', image: 'disk_1' }],
+ luns: [{ pool: 'rbd', image: 'disk_1', lun: 0 }],
auth: {
user: 'myiscsiusername',
password: 'myiscsipassword',
component.onImageSelection({ option: { name: 'rbd/disk_2', selected: true } });
expect(component.imagesSettings).toEqual({
'rbd/disk_2': {
+ lun: 0,
backstore: 'backstore:1',
'backstore:1': {}
}
expect(component.groups.controls[0].value).toEqual({ disks: [], group_id: 'foo', members: [] });
expect(component.imagesSettings).toEqual({
'rbd/disk_2': {
+ lun: 0,
backstore: 'backstore:1',
'backstore:1': {}
}
describe('should test initiators', () => {
beforeEach(() => {
+ component.onImageSelection({ option: { name: 'rbd/disk_2', selected: true } });
component.targetForm.patchValue({ disks: ['rbd/disk_2'], acl_enabled: true });
component.addGroup().patchValue({ name: 'group_1' });
component.addGroup().patchValue({ name: 'group_2' });
- component.onImageSelection({ option: { name: 'rbd/disk_2', selected: true } });
component.addInitiator();
component.initiators.controls[0].patchValue({
describe('should submit request', () => {
beforeEach(() => {
- component.targetForm.patchValue({ disks: ['rbd/disk_2'], acl_enabled: true });
component.onImageSelection({ option: { name: 'rbd/disk_2', selected: true } });
+ component.targetForm.patchValue({ disks: ['rbd/disk_2'], acl_enabled: true });
component.portals.setValue(['node1:192.168.100.201', 'node2:192.168.100.202']);
component.addInitiator().patchValue({
client_iqn: 'iqn.initiator'
luns: []
}
],
- disks: [{ backstore: 'backstore:1', controls: {}, image: 'disk_2', pool: 'rbd' }],
+ disks: [
+ {
+ backstore: 'backstore:1',
+ controls: {},
+ image: 'disk_2',
+ pool: 'rbd',
+ lun: 0,
+ wwn: undefined
+ }
+ ],
groups: [
{ disks: [{ image: 'disk_2', pool: 'rbd' }], group_id: 'foo', members: ['iqn.initiator'] }
],
luns: []
}
],
- disks: [{ backstore: 'backstore:1', controls: {}, image: 'disk_2', pool: 'rbd' }],
+ disks: [
+ {
+ backstore: 'backstore:1',
+ controls: {},
+ image: 'disk_2',
+ pool: 'rbd',
+ lun: 0,
+ wwn: undefined
+ }
+ ],
groups: [
{
disks: [{ image: 'disk_2', pool: 'rbd' }],
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual({
clients: [],
- disks: [{ backstore: 'backstore:1', controls: {}, image: 'disk_2', pool: 'rbd' }],
+ disks: [
+ {
+ backstore: 'backstore:1',
+ controls: {},
+ image: 'disk_2',
+ pool: 'rbd',
+ lun: 0,
+ wwn: undefined
+ }
+ ],
groups: [],
acl_enabled: false,
auth: {
cephIscsiConfigVersion: number;
targetForm: CdFormGroup;
modalRef: BsModalRef;
+ api_version = 0;
minimum_gateways = 1;
target_default_controls: any;
target_controls_limits: any;
.value();
// iscsiService.settings()
+ if ('api_version' in data[3]) {
+ this.api_version = data[3].api_version;
+ }
this.minimum_gateways = data[3].config.minimum_gateways;
this.target_default_controls = data[3].target_default_controls;
this.target_controls_limits = data[3].target_controls_limits;
})
]
}),
- disks: new FormControl([]),
+ disks: new FormControl([], {
+ validators: [
+ CdValidators.custom('dupLunId', (value) => {
+ const lunIds = this.getLunIds(value);
+ return lunIds.length !== _.uniq(lunIds).length;
+ }),
+ CdValidators.custom('dupWwn', (value) => {
+ const wwns = this.getWwns(value);
+ return wwns.length !== _.uniq(wwns).length;
+ })
+ ]
+ }),
initiators: new FormArray([]),
groups: new FormArray([]),
acl_enabled: new FormControl(false)
backstore: disk.backstore
};
this.imagesSettings[id][disk.backstore] = disk.controls;
+ if ('lun' in disk) {
+ this.imagesSettings[id]['lun'] = disk.lun;
+ }
+ if ('wwn' in disk) {
+ this.imagesSettings[id]['wwn'] = disk.wwn;
+ }
this.onImageSelection({ option: { name: id, selected: true } });
});
});
this.disks.value.splice(index, 1);
this.removeImageRefs(image);
+ this.targetForm.get('disks').updateValueAndValidity({ emitEvent: false });
return false;
}
return result;
}
+ isLunIdInUse(lunId, imageId) {
+ const images = this.disks.value.filter((currentImageId) => currentImageId !== imageId);
+ return this.getLunIds(images).includes(lunId);
+ }
+
+ getLunIds(images) {
+ return _.map(images, (image) => this.imagesSettings[image]['lun']);
+ }
+
+ nextLunId(imageId) {
+ const images = this.disks.value.filter((currentImageId) => currentImageId !== imageId);
+ const lunIdsInUse = this.getLunIds(images);
+ let lunIdCandidate = 0;
+ while (lunIdsInUse.includes(lunIdCandidate)) {
+ lunIdCandidate++;
+ }
+ return lunIdCandidate;
+ }
+
+ getWwns(images) {
+ const wwns = _.map(images, (image) => this.imagesSettings[image]['wwn']);
+ return wwns.filter((wwn) => _.isString(wwn) && wwn !== '');
+ }
+
onImageSelection($event) {
const option = $event.option;
if (!this.imagesSettings[option.name]) {
const defaultBackstore = this.getDefaultBackstore(option.name);
this.imagesSettings[option.name] = {
- backstore: defaultBackstore
+ backstore: defaultBackstore,
+ lun: this.nextLunId(option.name)
};
this.imagesSettings[option.name][defaultBackstore] = {};
+ } else if (this.isLunIdInUse(this.imagesSettings[option.name]['lun'], option.name)) {
+ // If the lun id is now in use, we have to generate a new one
+ this.imagesSettings[option.name]['lun'] = this.nextLunId(option.name);
}
_.forEach(this.imagesInitiatorSelections, (selections, i) => {
} else {
this.removeImageRefs(option.name);
}
+ this.targetForm.get('disks').updateValueAndValidity({ emitEvent: false });
}
// Initiators
pool: imageSplit[0],
image: imageSplit[1],
backstore: backstore,
- controls: this.imagesSettings[disk][backstore]
+ controls: this.imagesSettings[disk][backstore],
+ lun: this.imagesSettings[disk]['lun'],
+ wwn: this.imagesSettings[disk]['wwn']
});
});
const initialState = {
imagesSettings: this.imagesSettings,
image: image,
+ api_version: this.api_version,
disk_default_controls: this.disk_default_controls,
disk_controls_limits: this.disk_controls_limits,
- backstores: this.getValidBackstores(this.getImageById(image))
+ backstores: this.getValidBackstores(this.getImageById(image)),
+ control: this.targetForm.get('disks')
};
this.modalRef = this.modalService.show(IscsiTargetImageSettingsModalComponent, {
<cd-modal>
<ng-container class="modal-title">
- <ng-container i18n>Settings</ng-container>
+ <ng-container i18n>Configure</ng-container>
<small>{{ image }}</small>
</ng-container>
<p class="alert-warning"
i18n>Changing these parameters from their default values is usually not necessary.</p>
+ <span *ngIf="api_version >= 1">
+ <legend class="cd-header"
+ i18n>Identifier</legend>
+ <!-- LUN -->
+ <div class="form-group row">
+ <div class="col-sm-12">
+ <label class="col-form-label"
+ for="lun">
+ <ng-container i18n>lun</ng-container>
+ <span class="required"></span>
+ </label>
+ <input type="number"
+ class="form-control"
+ id="lun"
+ name="lun"
+ formControlName="lun">
+ <span class="invalid-feedback"
+ *ngIf="settingsForm.showError('lun', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ <!-- WWN -->
+ <div class="form-group row">
+ <div class="col-sm-12">
+ <label class="col-form-label"
+ for="wwn"
+ i18n>wwn</label>
+ <input type="text"
+ class="form-control"
+ id="wwn"
+ name="wwn"
+ formControlName="wwn">
+ </div>
+ </div>
+ </span>
+
+ <legend class="cd-header"
+ i18n>Settings</legend>
+
<!-- BACKSTORE -->
<div class="form-group row">
<div class="col-sm-12">
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { ReactiveFormsModule } from '@angular/forms';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { BsModalRef } from 'ngx-bootstrap/modal';
}
};
component.backstores = ['backstore:1', 'backstore:2'];
+ component.control = new FormControl();
component.ngOnInit();
fixture.detectChanges();
it('should fill the form', () => {
expect(component.settingsForm.value).toEqual({
+ lun: null,
+ wwn: null,
backstore: 'backstore:1',
foo: null,
bar: null,
component.save();
expect(component.imagesSettings).toEqual({
'rbd/disk_1': {
+ lun: null,
+ wwn: null,
backstore: 'backstore:1',
'backstore:1': {
foo: 1234
import { Component, OnInit } from '@angular/core';
-import { FormControl } from '@angular/forms';
+import { AbstractControl, FormControl } from '@angular/forms';
import * as _ from 'lodash';
import { BsModalRef } from 'ngx-bootstrap/modal';
export class IscsiTargetImageSettingsModalComponent implements OnInit {
image: string;
imagesSettings: any;
+ api_version: number;
disk_default_controls: any;
disk_controls_limits: any;
backstores: any;
+ control: AbstractControl;
settingsForm: CdFormGroup;
ngOnInit() {
const fg = {
- backstore: new FormControl(this.imagesSettings[this.image]['backstore'])
+ backstore: new FormControl(this.imagesSettings[this.image]['backstore']),
+ lun: new FormControl(this.imagesSettings[this.image]['lun']),
+ wwn: new FormControl(this.imagesSettings[this.image]['wwn'])
};
_.forEach(this.backstores, (backstore) => {
const model = this.imagesSettings[this.image][backstore] || {};
save() {
const backstore = this.settingsForm.controls['backstore'].value;
+ const lun = this.settingsForm.controls['lun'].value;
+ const wwn = this.settingsForm.controls['wwn'].value;
const settings = {};
_.forIn(this.settingsForm.controls, (control, key) => {
if (
}
});
this.imagesSettings[this.image]['backstore'] = backstore;
+ this.imagesSettings[this.image]['lun'] = lun;
+ this.imagesSettings[this.image]['wwn'] = wwn;
this.imagesSettings[this.image][backstore] = settings;
this.imagesSettings = { ...this.imagesSettings };
+ this.control.updateValueAndValidity({ emitEvent: false });
this.modalRef.hide();
}
}
color: $color-required-text;
}
/* Forms */
-.form-group > .control-label > span.required {
+.form-group > .control-label > span.required,
+.form-group > .col-sm-12 > .control-label > span.required {
@extend .fa;
@extend .fa-asterisk;
@extend .required;
return request()
@RestClient.api_put('/api/disk/{pool}/{image}')
- def create_disk(self, pool, image, backstore, request=None):
+ def create_disk(self, pool, image, backstore, wwn, request=None):
logger.debug("iSCSI[%s] Creating disk: %s/%s", self.gateway_name, pool, image)
return request({
'mode': 'create',
- 'backstore': backstore
+ 'backstore': backstore,
+ 'wwn': wwn
})
@RestClient.api_delete('/api/disk/{pool}/{image}')
})
@RestClient.api_put('/api/targetlun/{target_iqn}')
- def create_target_lun(self, target_iqn, image_id, request=None):
+ def create_target_lun(self, target_iqn, image_id, lun, request=None):
logger.debug("iSCSI[%s] Creating target lun: %s/%s", self.gateway_name, target_iqn,
image_id)
return request({
- 'disk': image_id
+ 'disk': image_id,
+ 'lun_id': lun
})
@RestClient.api_delete('/api/targetlun/{target_iqn}')
'disk': image_id
})
+ @RestClient.api_delete('/api/clientlun/{target_iqn}/{client_iqn}')
+ def delete_client_lun(self, target_iqn, client_iqn, image_id, request=None):
+ logger.debug("iSCSI[%s] Deleting client lun: %s/%s", self.gateway_name, target_iqn,
+ client_iqn)
+ return request({
+ 'disk': image_id
+ })
+
@RestClient.api_put('/api/clientauth/{target_iqn}/{client_iqn}')
def create_client_auth(self, target_iqn, client_iqn, username, password, mutual_username,
mutual_password, request=None):
target_config['ip_list'].remove(ip)
target_config['portals'].pop(gateway_name)
- def create_disk(self, pool, image, backstore):
+ def create_disk(self, pool, image, backstore, wwn):
+ if wwn is None:
+ wwn = '64af6678-9694-4367-bacc-f8eb0baa' + str(len(self.config['disks']))
image_id = '{}/{}'.format(pool, image)
self.config['disks'][image_id] = {
"pool": pool,
"image": image,
"backstore": backstore,
"controls": {},
- "wwn": '64af6678-9694-4367-bacc-f8eb0baa' + str(len(self.config['disks']))
+ "wwn": wwn
}
- def create_target_lun(self, target_iqn, image_id):
+ def create_target_lun(self, target_iqn, image_id, lun):
target_config = self.config['targets'][target_iqn]
+ if lun is None:
+ lun = len(target_config['disks'])
target_config['disks'][image_id] = {
- "lun_id": len(target_config['disks'])
+ "lun_id": lun
}
self.config['disks'][image_id]['owner'] = list(target_config['portals'].keys())[0]
target_config = self.config['targets'][target_iqn]
target_config['clients'][client_iqn]['luns'][image_id] = {}
+ def delete_client_lun(self, target_iqn, client_iqn, image_id):
+ target_config = self.config['targets'][target_iqn]
+ del target_config['clients'][client_iqn]['luns'][image_id]
+
def create_client_auth(self, target_iqn, client_iqn, user, password, m_user, m_password):
target_config = self.config['targets'][target_iqn]
target_config['clients'][client_iqn]['auth']['username'] = user