]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Set iSCSI disk WWN and LUN number from the UI
authorRicardo Marques <rimarques@suse.com>
Fri, 18 Oct 2019 16:43:38 +0000 (17:43 +0100)
committerRicardo Marques <rimarques@suse.com>
Thu, 7 Nov 2019 15:38:16 +0000 (15:38 +0000)
Fixes: https://tracker.ceph.com/issues/41749
Signed-off-by: Ricardo Marques <rimarques@suse.com>
(cherry picked from commit 970c1253c8a73ec31363d9d4cbe5fdf008fb61f5)

 Conflicts:
       src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.html
       src/pybind/mgr/dashboard/frontend/src/styles.scss

Conflicts caused by "Master" using Bootstrap 4, but "Nautilus" using Bootstap 3.

src/pybind/mgr/dashboard/controllers/iscsi.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.ts
src/pybind/mgr/dashboard/frontend/src/styles.scss
src/pybind/mgr/dashboard/services/iscsi_client.py
src/pybind/mgr/dashboard/tests/test_iscsi.py

index fd8633db43f85f94e6e9be6629e6bc2342f5f30b..ca0eebf7d990ffee7de232bb78a76fad8545edda 100644 (file)
@@ -321,10 +321,12 @@ class IscsiTarget(RESTController):
                 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)
@@ -332,6 +334,14 @@ class IscsiTarget(RESTController):
             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)
@@ -633,13 +643,17 @@ class IscsiTarget(RESTController):
                 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 = {}
@@ -661,18 +675,20 @@ class IscsiTarget(RESTController):
                 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']
index 6944dda3c6f7afe2d231409dc8271a7d65512c12..0562efa32d700ab90a02ec17a4ee39f991623020 100644 (file)
                        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"
index da510c86107f4d1d107abf8b4065351744e229da..e95f5dc2065555d861e6c1343cea8ab15af4429e 100644 (file)
@@ -42,18 +42,27 @@ describe('IscsiTargetFormComponent', () => {
       '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',
@@ -205,6 +214,7 @@ describe('IscsiTargetFormComponent', () => {
     component.onImageSelection({ option: { name: 'rbd/disk_2', selected: true } });
     expect(component.imagesSettings).toEqual({
       'rbd/disk_2': {
+        lun: 0,
         backstore: 'backstore:1',
         'backstore:1': {}
       }
@@ -230,6 +240,7 @@ describe('IscsiTargetFormComponent', () => {
     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': {}
       }
@@ -238,10 +249,10 @@ describe('IscsiTargetFormComponent', () => {
 
   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({
@@ -356,8 +367,8 @@ describe('IscsiTargetFormComponent', () => {
 
   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'
@@ -386,7 +397,16 @@ describe('IscsiTargetFormComponent', () => {
             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'] }
         ],
@@ -420,7 +440,16 @@ describe('IscsiTargetFormComponent', () => {
             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' }],
@@ -452,7 +481,16 @@ describe('IscsiTargetFormComponent', () => {
       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: {
index d206010f8070090b97be51f0439b15d840fb4d35..3b88e20dd8a60afeeb86ab90681a1f9af158fda8 100644 (file)
@@ -28,6 +28,7 @@ export class IscsiTargetFormComponent implements OnInit {
   cephIscsiConfigVersion: number;
   targetForm: CdFormGroup;
   modalRef: BsModalRef;
+  api_version = 0;
   minimum_gateways = 1;
   target_default_controls: any;
   target_controls_limits: any;
@@ -123,6 +124,9 @@ export class IscsiTargetFormComponent implements OnInit {
         .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;
@@ -188,7 +192,18 @@ export class IscsiTargetFormComponent implements OnInit {
           })
         ]
       }),
-      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)
@@ -235,6 +250,12 @@ export class IscsiTargetFormComponent implements OnInit {
         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 } });
     });
@@ -297,6 +318,7 @@ export class IscsiTargetFormComponent implements OnInit {
     });
     this.disks.value.splice(index, 1);
     this.removeImageRefs(image);
+    this.targetForm.get('disks').updateValueAndValidity({ emitEvent: false });
     return false;
   }
 
@@ -334,6 +356,30 @@ export class IscsiTargetFormComponent implements OnInit {
     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;
 
@@ -341,9 +387,13 @@ export class IscsiTargetFormComponent implements OnInit {
       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) => {
@@ -358,6 +408,7 @@ export class IscsiTargetFormComponent implements OnInit {
     } else {
       this.removeImageRefs(option.name);
     }
+    this.targetForm.get('disks').updateValueAndValidity({ emitEvent: false });
   }
 
   // Initiators
@@ -624,7 +675,9 @@ export class IscsiTargetFormComponent implements OnInit {
         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']
       });
     });
 
@@ -727,9 +780,11 @@ export class IscsiTargetFormComponent implements OnInit {
     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, {
index 8970e59a76e4b1d212fed40c4d2b7e7db4fce037..e2d6710d2a724a0f77ed39083dd40b27c82d5e02 100644 (file)
@@ -1,6 +1,6 @@
 <cd-modal>
   <ng-container class="modal-title">
-    <ng-container i18n>Settings</ng-container>&nbsp;
+    <ng-container i18n>Configure</ng-container>&nbsp;
     <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">
index 4bb02ace02cd9bfd7160902d96130d1adb966a5c..921292bcdb046a4e9f862b88b147b616e6684326 100644 (file)
@@ -1,6 +1,6 @@
 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';
@@ -57,6 +57,7 @@ describe('IscsiTargetImageSettingsModalComponent', () => {
       }
     };
     component.backstores = ['backstore:1', 'backstore:2'];
+    component.control = new FormControl();
 
     component.ngOnInit();
     fixture.detectChanges();
@@ -68,6 +69,8 @@ describe('IscsiTargetImageSettingsModalComponent', () => {
 
   it('should fill the form', () => {
     expect(component.settingsForm.value).toEqual({
+      lun: null,
+      wwn: null,
       backstore: 'backstore:1',
       foo: null,
       bar: null,
@@ -83,6 +86,8 @@ describe('IscsiTargetImageSettingsModalComponent', () => {
     component.save();
     expect(component.imagesSettings).toEqual({
       'rbd/disk_1': {
+        lun: null,
+        wwn: null,
         backstore: 'backstore:1',
         'backstore:1': {
           foo: 1234
index 3918aabfce195ea182d85ca4398bfe04fd541f10..ee021be85ed64e60d07b245334108e1041e1d103 100644 (file)
@@ -1,5 +1,5 @@
 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';
@@ -15,9 +15,11 @@ import { CdFormGroup } from '../../../shared/forms/cd-form-group';
 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;
 
@@ -25,7 +27,9 @@ export class IscsiTargetImageSettingsModalComponent implements OnInit {
 
   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] || {};
@@ -43,6 +47,8 @@ export class IscsiTargetImageSettingsModalComponent implements OnInit {
 
   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 (
@@ -62,8 +68,11 @@ export class IscsiTargetImageSettingsModalComponent implements OnInit {
       }
     });
     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();
   }
 }
index 2e14f32978453ba526a0bc03b04810c06748f9a9..ad06737c98c14c3bd7bed5b08eae7cb532b4579f 100644 (file)
@@ -309,7 +309,8 @@ uib-accordion .panel-title,
   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;
index 593622608012610ff7936ab3f0525ca7a59f87db..726a32c29daedeeeb15f446c0a2e797776f7895e 100644 (file)
@@ -115,11 +115,12 @@ class IscsiClient(RestClient):
         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}')
@@ -138,11 +139,12 @@ class IscsiClient(RestClient):
         })
 
     @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}')
@@ -171,6 +173,14 @@ class IscsiClient(RestClient):
             '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):
index d0d86e32115f2514857cd22e7c12815dd3266dc8..d7262ebedbafe06f1004ecc27347528bc726a622 100644 (file)
@@ -603,20 +603,24 @@ class IscsiClientMock(object):
             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]
 
@@ -650,6 +654,10 @@ class IscsiClientMock(object):
         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