@UiApiController('/iscsi', Scope.ISCSI)
class IscsiUi(BaseController):
- REQUIRED_CEPH_ISCSI_CONFIG_VERSION = 10
+ REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION = 10
+ REQUIRED_CEPH_ISCSI_CONFIG_MAX_VERSION = 11
@Endpoint()
@ReadPermission
status['message'] = 'Gateway {} is inaccessible'.format(gateway)
return status
config = IscsiClient.instance().get_config()
- if config['version'] != IscsiUi.REQUIRED_CEPH_ISCSI_CONFIG_VERSION:
- status['message'] = 'Unsupported `ceph-iscsi` config version. Expected {} but ' \
- 'found {}.'.format(IscsiUi.REQUIRED_CEPH_ISCSI_CONFIG_VERSION,
- config['version'])
+ if config['version'] < IscsiUi.REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION or \
+ config['version'] > IscsiUi.REQUIRED_CEPH_ISCSI_CONFIG_MAX_VERSION:
+ status['message'] = 'Unsupported `ceph-iscsi` config version. ' \
+ 'Expected >= {} and <= {} but found' \
+ ' {}.'.format(IscsiUi.REQUIRED_CEPH_ISCSI_CONFIG_MIN_VERSION,
+ IscsiUi.REQUIRED_CEPH_ISCSI_CONFIG_MAX_VERSION,
+ config['version'])
return status
status['available'] = True
except RequestException as e:
return status
+ @Endpoint()
+ @ReadPermission
+ def version(self):
+ return {
+ 'ceph_iscsi_config_version': IscsiClient.instance().get_config()['version']
+ }
+
@Endpoint()
@ReadPermission
def settings(self):
@iscsi_target_task('create', {'target_iqn': '{target_iqn}'})
def create(self, target_iqn=None, target_controls=None, acl_enabled=None,
- portals=None, disks=None, clients=None, groups=None):
+ auth=None, portals=None, disks=None, clients=None, groups=None):
target_controls = target_controls or {}
portals = portals or []
disks = disks or []
component='iscsi')
settings = IscsiClient.instance().get_settings()
IscsiTarget._validate(target_iqn, target_controls, portals, disks, groups, settings)
- IscsiTarget._create(target_iqn, target_controls, acl_enabled, portals, disks, clients,
- groups, 0, 100, config, settings)
+
+ IscsiTarget._create(target_iqn, target_controls, acl_enabled, auth, portals, disks,
+ clients, groups, 0, 100, config, settings)
@iscsi_target_task('edit', {'target_iqn': '{target_iqn}'})
def set(self, target_iqn, new_target_iqn=None, target_controls=None, acl_enabled=None,
- portals=None, disks=None, clients=None, groups=None):
+ auth=None, portals=None, disks=None, clients=None, groups=None):
target_controls = target_controls or {}
portals = IscsiTarget._sorted_portals(portals)
disks = IscsiTarget._sorted_disks(disks)
IscsiTarget._validate(new_target_iqn, target_controls, portals, disks, groups, settings)
config = IscsiTarget._delete(target_iqn, config, 0, 50, new_target_iqn, target_controls,
portals, disks, clients, groups)
- IscsiTarget._create(new_target_iqn, target_controls, acl_enabled, portals, disks, clients,
- groups, 50, 100, config, settings)
+ IscsiTarget._create(new_target_iqn, target_controls, acl_enabled, auth, portals, disks,
+ clients, groups, 50, 100, config, settings)
@staticmethod
def _delete(target_iqn, config, task_progress_begin, task_progress_end, new_target_iqn=None,
code='pool_does_not_exist',
component='iscsi')
+ @staticmethod
+ def _update_targetauth(config, target_iqn, auth, gateway_name):
+ # Target level authentication was introduced in ceph-iscsi config v11
+ if config['version'] > 10:
+ user = auth['user']
+ password = auth['password']
+ mutual_user = auth['mutual_user']
+ mutual_password = auth['mutual_password']
+ IscsiClient.instance(gateway_name=gateway_name).update_targetauth(target_iqn,
+ user,
+ password,
+ mutual_user,
+ mutual_password)
+
+ @staticmethod
+ def _update_targetacl(target_config, target_iqn, acl_enabled, gateway_name):
+ if not target_config or target_config['acl_enabled'] != acl_enabled:
+ targetauth_action = ('enable_acl' if acl_enabled else 'disable_acl')
+ IscsiClient.instance(gateway_name=gateway_name).update_targetacl(target_iqn,
+ targetauth_action)
+
@staticmethod
def _create(target_iqn, target_controls, acl_enabled,
- portals, disks, clients, groups,
+ auth, portals, disks, clients, groups,
task_progress_begin, task_progress_end, config, settings):
target_config = config['targets'].get(target_iqn, None)
TaskManager.current_task().set_progress(task_progress_begin)
host,
ip_list)
TaskManager.current_task().inc_progress(task_progress_inc)
- targetauth_action = ('enable_acl' if acl_enabled else 'disable_acl')
- IscsiClient.instance(gateway_name=gateway_name).update_targetauth(target_iqn,
- targetauth_action)
+
+ if acl_enabled:
+ IscsiTarget._update_targetauth(config, target_iqn, auth, gateway_name)
+ IscsiTarget._update_targetacl(target_config, target_iqn, acl_enabled, gateway_name)
+
+ else:
+ IscsiTarget._update_targetacl(target_config, target_iqn, acl_enabled, gateway_name)
+ IscsiTarget._update_targetauth(config, target_iqn, auth, gateway_name)
+
for disk in disks:
pool = disk['pool']
image = disk['image']
'target_controls': target_controls,
'acl_enabled': acl_enabled
}
+ # Target level authentication was introduced in ceph-iscsi config v11
+ if config['version'] > 10:
+ target_user = target_config['auth']['username']
+ target_password = target_config['auth']['password']
+ target_mutual_user = target_config['auth']['mutual_username']
+ target_mutual_password = target_config['auth']['mutual_password']
+ target['auth'] = {
+ 'user': target_user,
+ 'password': target_password,
+ 'mutual_user': target_mutual_user,
+ 'mutual_password': target_mutual_password
+ }
return target
@staticmethod
selection: CdTableSelection;
@Input()
settings: any;
+ @Input()
+ cephIscsiConfigVersion: number;
@ViewChild('highlightTpl')
highlightTpl: TemplateRef<any>;
}
private generateTree() {
- this.metadata = { root: this.selectedItem.target_controls };
-
+ const target_meta = _.cloneDeep(this.selectedItem.target_controls);
+ // Target level authentication was introduced in ceph-iscsi config v11
+ if (this.cephIscsiConfigVersion > 10) {
+ _.extend(target_meta, _.cloneDeep(this.selectedItem.auth));
+ }
+ this.metadata = { root: target_meta };
const cssClasses = {
target: {
expanded: this.selectedItem.cdExecuting
current: tempData[key] || value
};
});
+ // Target level authentication was introduced in ceph-iscsi config v11
+ if (this.cephIscsiConfigVersion > 10) {
+ ['user', 'password', 'mutual_user', 'mutual_password'].forEach((key) => {
+ this.data.push({
+ displayName: key,
+ default: null,
+ current: tempData[key]
+ });
+ });
+ }
} else if (e.node.id.toString().startsWith('disk_')) {
this.columns[2].isHidden = false;
this.data = _.map(this.settings.disk_default_controls[tempData.backstore], (value, key) => {
</div>
</div>
+ <!-- Target level authentication was introduced in ceph-iscsi config v11 -->
+ <div formGroupName="auth" *ngIf="cephIscsiConfigVersion > 10 && !targetForm.getValue('acl_enabled')">
+
+ <!-- Target user -->
+ <div class="form-group"
+ [ngClass]="{'has-error': targetForm.showError('user', formDir)}">
+ <label class="control-label col-sm-3"
+ for="target_user">
+ <ng-container i18n>User</ng-container>
+ </label>
+ <div class="col-sm-9">
+ <input class="form-control"
+ type="text"
+ id="target_user"
+ name="target_user"
+ formControlName="user" />
+
+ <span class="help-block"
+ *ngIf="targetForm.showError('user', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="help-block"
+ *ngIf="targetForm.showError('user', formDir, 'pattern')"
+ i18n>Usernames must have a length of 8 to 64 characters and
+ can only contain letters, '.', '@', '-', '_' or ':'.</span>
+ </div>
+ </div>
+
+ <!-- Target password -->
+ <div class="form-group"
+ [ngClass]="{'has-error': targetForm.showError('password', formDir)}">
+ <label class="control-label col-sm-3"
+ for="target_password">
+ <ng-container i18n>Password</ng-container>
+ </label>
+ <div class="col-sm-9">
+ <div class="input-group">
+ <input class="form-control"
+ type="password"
+ autocomplete="new-password"
+ id="target_password"
+ name="target_password"
+ formControlName="password" />
+
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default"
+ cdPasswordButton="target_password">
+ </button>
+ <button type="button"
+ class="btn btn-default"
+ cdCopy2ClipboardButton="target_password">
+ </button>
+ </span>
+ </div>
+
+ <span class="help-block"
+ *ngIf="targetForm.showError('password', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="help-block"
+ *ngIf="targetForm.showError('password', formDir, 'pattern')"
+ i18n>Passwords must have a length of 12 to 16 characters
+ and can only contain letters, '@', '-', '_' or '/'.</span>
+ </div>
+ </div>
+
+ <!-- Target mutual_user -->
+ <div class="form-group"
+ [ngClass]="{'has-error': targetForm.showError('mutual_user', formDir)}">
+ <label class="control-label col-sm-3"
+ for="target_mutual_user">
+ <ng-container i18n>Mutual User</ng-container>
+ </label>
+ <div class="col-sm-9">
+ <input class="form-control"
+ type="text"
+ id="target_mutual_user"
+ name="target_mutual_user"
+ formControlName="mutual_user" />
+
+ <span class="help-block"
+ *ngIf="targetForm.showError('mutual_user', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="help-block"
+ *ngIf="targetForm.showError('mutual_user', formDir, 'pattern')"
+ i18n>Usernames must have a length of 8 to 64 characters and
+ can only contain letters, '.', '@', '-', '_' or ':'.</span>
+ </div>
+ </div>
+
+ <!-- Target mutual_password -->
+ <div class="form-group"
+ [ngClass]="{'has-error': targetForm.showError('mutual_password', formDir)}">
+ <label class="control-label col-sm-3"
+ for="target_mutual_password">
+ <ng-container i18n>Mutual Password</ng-container>
+ </label>
+ <div class="col-sm-9">
+ <div class="input-group">
+ <input class="form-control"
+ type="password"
+ autocomplete="new-password"
+ id="target_mutual_password"
+ name="target_mutual_password"
+ formControlName="mutual_password" />
+
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default"
+ cdPasswordButton="target_mutual_password">
+ </button>
+ <button type="button"
+ class="btn btn-default"
+ cdCopy2ClipboardButton="target_mutual_password">
+ </button>
+ </span>
+ </div>
+
+ <span class="help-block"
+ *ngIf="targetForm.showError('mutual_password', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="help-block"
+ *ngIf="targetForm.showError('mutual_password', formDir, 'pattern')"
+ i18n>Passwords must have a length of 12 to 16 characters
+ and can only contain letters, '@', '-', '_' or '/'.</span>
+ </div>
+ </div>
+
+ </div>
+
<!-- Initiators -->
<div class="form-group"
*ngIf="targetForm.getValue('acl_enabled')">
{ name: 'node2', ip_addresses: ['192.168.100.202'] }
];
+ const VERSION = {
+ ceph_iscsi_config_version: 11
+ };
+
const RBD_LIST = [
{ status: 0, value: [], pool_name: 'ganesha' },
{
httpTesting.expectOne('ui-api/iscsi/settings').flush(SETTINGS);
httpTesting.expectOne('ui-api/iscsi/portals').flush(PORTALS);
+ httpTesting.expectOne('ui-api/iscsi/version').flush(VERSION);
httpTesting.expectOne('api/summary').flush({});
httpTesting.expectOne('api/block/image').flush(RBD_LIST);
httpTesting.expectOne('api/iscsi/target').flush(LIST_TARGET);
groups: [],
initiators: [],
acl_enabled: false,
+ auth: {
+ password: '',
+ user: '',
+ mutual_password: '',
+ mutual_user: ''
+ },
portals: [],
target_controls: {},
target_iqn: component.targetForm.value.target_iqn
],
target_controls: {},
target_iqn: component.target_iqn,
- acl_enabled: true
+ acl_enabled: true,
+ auth: {
+ password: '',
+ user: '',
+ mutual_password: '',
+ mutual_user: ''
+ }
});
});
],
target_controls: {},
target_iqn: component.targetForm.value.target_iqn,
- acl_enabled: true
+ acl_enabled: true,
+ auth: {
+ password: '',
+ user: '',
+ mutual_password: '',
+ mutual_user: ''
+ }
});
});
disks: [{ backstore: 'backstore:1', controls: {}, image: 'disk_2', pool: 'rbd' }],
groups: [],
acl_enabled: false,
+ auth: {
+ password: '',
+ user: '',
+ mutual_password: '',
+ mutual_user: ''
+ },
portals: [
{ host: 'node1', ip: '192.168.100.201' },
{ host: 'node2', ip: '192.168.100.202' }
styleUrls: ['./iscsi-target-form.component.scss']
})
export class IscsiTargetFormComponent implements OnInit {
+ cephIscsiConfigVersion: number;
targetForm: CdFormGroup;
modalRef: BsModalRef;
minimum_gateways = 1;
this.iscsiService.listTargets(),
this.rbdService.list(),
this.iscsiService.portals(),
- this.iscsiService.settings()
+ this.iscsiService.settings(),
+ this.iscsiService.version()
];
if (this.router.url.startsWith('/block/iscsi/targets/edit')) {
});
this.portalsSelections = [...portals];
+ // iscsiService.version()
+ this.cephIscsiConfigVersion = data[4]['ceph_iscsi_config_version'];
+
this.createForm();
// iscsiService.getTarget()
- if (data[4]) {
- this.resolveModel(data[4]);
+ if (data[5]) {
+ this.resolveModel(data[5]);
}
});
}
groups: new FormArray([]),
acl_enabled: new FormControl(false)
});
+ // Target level authentication was introduced in ceph-iscsi config v11
+ if (this.cephIscsiConfigVersion > 10) {
+ const authFormGroup = new CdFormGroup({
+ user: new FormControl(''),
+ password: new FormControl(''),
+ mutual_user: new FormControl(''),
+ mutual_password: new FormControl('')
+ });
+ this.setAuthValidator(authFormGroup);
+ this.targetForm.addControl('auth', authFormGroup);
+ }
}
resolveModel(res) {
target_controls: res.target_controls,
acl_enabled: res.acl_enabled
});
-
+ // Target level authentication was introduced in ceph-iscsi config v11
+ if (this.cephIscsiConfigVersion > 10) {
+ this.targetForm.patchValue({
+ auth: res.auth
+ });
+ }
const portals = [];
_.forEach(res.portals, (portal) => {
const id = `${portal.host}:${portal.ip}`;
cdIsInGroup: new FormControl(false)
});
+ this.setAuthValidator(fg);
+
+ this.initiators.push(fg);
+
+ _.forEach(this.groupMembersSelections, (selections, i) => {
+ selections.push(new SelectOption(false, '', ''));
+ this.groupMembersSelections[i] = [...selections];
+ });
+
+ const disks = _.map(
+ this.targetForm.getValue('disks'),
+ (disk) => new SelectOption(false, disk, '')
+ );
+ this.imagesInitiatorSelections.push(disks);
+
+ return fg;
+ }
+
+ setAuthValidator(fg: CdFormGroup) {
CdValidators.validateIf(
fg.get('user'),
() => fg.getValue('password') || fg.getValue('mutual_user') || fg.getValue('mutual_password'),
[Validators.pattern(this.PASSWORD_REGEX)],
[fg.get('user'), fg.get('password'), fg.get('mutual_user')]
);
-
- this.initiators.push(fg);
-
- _.forEach(this.groupMembersSelections, (selections, i) => {
- selections.push(new SelectOption(false, '', ''));
- this.groupMembersSelections[i] = [...selections];
- });
-
- const disks = _.map(
- this.targetForm.getValue('disks'),
- (disk) => new SelectOption(false, disk, '')
- );
- this.imagesInitiatorSelections.push(disks);
-
- return fg;
}
removeInitiator(index) {
groups: []
};
+ // Target level authentication was introduced in ceph-iscsi config v11
+ if (this.cephIscsiConfigVersion > 10) {
+ const targetAuth: CdFormGroup = this.targetForm.get('auth') as CdFormGroup;
+ if (!targetAuth.getValue('user')) {
+ targetAuth.get('user').setValue('');
+ }
+ if (!targetAuth.getValue('password')) {
+ targetAuth.get('password').setValue('');
+ }
+ if (!targetAuth.getValue('mutual_user')) {
+ targetAuth.get('mutual_user').setValue('');
+ }
+ if (!targetAuth.getValue('mutual_password')) {
+ targetAuth.get('mutual_password').setValue('');
+ }
+ const acl_enabled = this.targetForm.getValue('acl_enabled');
+ request['auth'] = {
+ user: acl_enabled ? '' : targetAuth.getValue('user'),
+ password: acl_enabled ? '' : targetAuth.getValue('password'),
+ mutual_user: acl_enabled ? '' : targetAuth.getValue('mutual_user'),
+ mutual_password: acl_enabled ? '' : targetAuth.getValue('mutual_password')
+ };
+ }
+
// Disks
formValue.disks.forEach((disk) => {
const imageSplit = disk.split('/');
<cd-iscsi-target-details cdTableDetail
*ngIf="selection.hasSingleSelection"
+ [cephIscsiConfigVersion]="cephIscsiConfigVersion"
[selection]="selection"
[settings]="settings"></cd-iscsi-target-details>
</cd-table>
summaryService['summaryData$'] = summaryService['summaryDataSource'].asObservable();
spyOn(iscsiService, 'status').and.returnValue(of({ available: true }));
+ spyOn(iscsiService, 'version').and.returnValue(of({ ceph_iscsi_config_version: 11 }));
});
it('should create', () => {
modalRef: BsModalRef;
permissions: Permissions;
selection = new CdTableSelection();
+ cephIscsiConfigVersion: number;
settings: any;
status: string;
summaryDataSubscription: Subscription;
this.available = result.available;
if (result.available) {
- this.taskListService.init(
- () => this.iscsiService.listTargets(),
- (resp) => this.prepareResponse(resp),
- (targets) => (this.targets = targets),
- () => this.onFetchError(),
- this.taskFilter,
- this.itemFilter,
- this.builders
- );
+ this.iscsiService.version().subscribe((res: any) => {
+ this.cephIscsiConfigVersion = res['ceph_iscsi_config_version'];
+ this.taskListService.init(
+ () => this.iscsiService.listTargets(),
+ (resp) => this.prepareResponse(resp),
+ (targets) => (this.targets = targets),
+ () => this.onFetchError(),
+ this.taskFilter,
+ this.itemFilter,
+ this.builders
+ );
+ });
this.iscsiService.settings().subscribe((settings: any) => {
this.settings = settings;
return this.http.get(`ui-api/iscsi/settings`);
}
+ version() {
+ return this.http.get(`ui-api/iscsi/version`);
+ }
+
portals() {
return this.http.get(`ui-api/iscsi/portals`);
}
})
@RestClient.api_put('/api/targetauth/{target_iqn}')
- def update_targetauth(self, target_iqn, action, request=None):
- logger.debug("iSCSI[%s] Updating targetauth: %s/%s", self.gateway_name, target_iqn, action)
+ def update_targetacl(self, target_iqn, action, request=None):
+ logger.debug("iSCSI[%s] Updating targetacl: %s/%s", self.gateway_name, target_iqn, action)
return request({
'action': action
})
+ @RestClient.api_put('/api/targetauth/{target_iqn}')
+ def update_targetauth(self, target_iqn, user, password, mutual_user, mutual_password,
+ request=None):
+ logger.debug("iSCSI[%s] Updating targetauth: %s/%s/%s/%s/%s", self.gateway_name,
+ target_iqn, user, password, mutual_user, mutual_password)
+ return request({
+ 'username': user,
+ 'password': password,
+ 'mutual_username': mutual_user,
+ 'mutual_password': mutual_password
+ })
+
@RestClient.api_get('/api/targetinfo/{target_iqn}')
def get_targetinfo(self, target_iqn, request=None):
# pylint: disable=unused-argument
}
],
"acl_enabled": True,
+ "auth": {
+ "password": "",
+ "user": "",
+ "mutual_password": "",
+ "mutual_user": ""},
"target_controls": {},
"groups": [
{
}
],
"acl_enabled": True,
+ "auth": {
+ "password": "",
+ "user": "",
+ "mutual_password": "",
+ "mutual_user": ""},
'groups': [
{
'group_id': 'mygroup',
"gateways": {},
"targets": {},
"updated": "",
- "version": 9
+ "version": 11
}
@classmethod
self.config['targets'][target_iqn] = {
"clients": {},
"acl_enabled": True,
+ "auth": {
+ "username": "",
+ "password": "",
+ "password_encryption_enabled": False,
+ "mutual_username": "",
+ "mutual_password": "",
+ "mutual_password_encryption_enabled": False
+ },
"controls": target_controls,
"created": "2019/01/17 09:22:34",
"disks": [],
self.config['discovery_auth']['mutual_username'] = mutual_user
self.config['discovery_auth']['mutual_password'] = mutual_password
- def update_targetauth(self, target_iqn, action):
+ def update_targetacl(self, target_iqn, action):
self.config['targets'][target_iqn]['acl_enabled'] = (action == 'enable_acl')
+ def update_targetauth(self, target_iqn, user, password, mutual_user, mutual_password):
+ target_config = self.config['targets'][target_iqn]
+ target_config['auth']['username'] = user
+ target_config['auth']['password'] = password
+ target_config['auth']['mutual_username'] = mutual_user
+ target_config['auth']['mutual_password'] = mutual_password
+
def get_targetinfo(self, target_iqn):
# pylint: disable=unused-argument
return {