23 changes: 19 additions & 4 deletions gwcli/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,8 @@ def ui_command_delete(self, gateway_name, confirm=None):

self.logger.info("Deleting gateway, {}".format(gateway_name))

confirm = self.ui_eval_param(confirm, 'bool', False)

config = self.parent.parent.parent._get_config()
if not config:
self.logger.error("Unable to refresh local config over API - sync "
Expand All @@ -715,7 +717,6 @@ def ui_command_delete(self, gateway_name, confirm=None):
return

if gw_cnt == 1:
confirm = self.ui_eval_param(confirm, 'bool', False)
if not confirm:
self.logger.error("Deleting the last gateway will remove all "
"objects on this target. Use confirm=true")
Expand All @@ -724,24 +725,38 @@ def ui_command_delete(self, gateway_name, confirm=None):
gw_api = '{}://{}:{}/api'.format(self.http_mode, "localhost",
settings.config.api_port)
gw_rqst = gw_api + '/gateway/{}/{}'.format(target_iqn, gateway_name)
if confirm:
gw_vars = {"force": 'true'}
else:
gw_vars = {"force": 'false'}

api = APIRequest(gw_rqst)
api = APIRequest(gw_rqst, data=gw_vars)
api.delete()

msg = response_message(api.response, self.logger)
if api.response.status_code != 200:
self.logger.error("Failed : {}".format(msg))
if "unavailable:" + gateway_name in msg:
self.logger.error("Could not contact {}. If the gateway is "
"permanently down. Use confirm=true to "
"force removal. WARNING: Forcing removal of "
"a gateway that can still be reached by an "
"initiator may result in data corruption.".
format(gateway_name))
else:
self.logger.error("Failed : {}".format(msg))
return

self.logger.debug("{}".format(msg))
self.logger.debug("Removing gw from UI")

self.thread_lock.acquire()
gw_object = self.get_child(gateway_name)
self.remove_child(gw_object)
self.thread_lock.release()

config = self.parent.parent.parent._get_config()
if not config:
self.logger.error("Could not refresh disaply. Restart gwcli.")
self.logger.error("Could not refresh display. Restart gwcli.")
elif not config['targets'][target_iqn]['portals']:
# no more gws so everything but the target is dropped.
disks_object = self.parent.get_child("disks")
Expand Down
2 changes: 1 addition & 1 deletion gwcli/hostgroup.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ def ui_command_disk(self, action, disk_name):
mapped_disks = [mapped_disk.name
for mapped_disk in self.parent.parent.target_disks.children]
if disk_name not in mapped_disks:
rc = self.parent.parent.target_disks.add_disk(disk_name, None)
rc = self.parent.parent.target_disks.add_disk(disk_name, None, None)
if rc == 0:
self.logger.debug("disk auto-map successful")
else:
Expand Down
36 changes: 21 additions & 15 deletions gwcli/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def reset(self):
for child in children:
self.remove_child(child)

def ui_command_attach(self, pool=None, image=None, backstore=None):
def ui_command_attach(self, pool=None, image=None, backstore=None, wwn=None):
"""
Assign a previously created RBD image to the gateway(s)
Expand Down Expand Up @@ -187,9 +187,10 @@ def ui_command_attach(self, pool=None, image=None, backstore=None):

self.logger.debug("CMD: /disks/ attach pool={} "
"image={}".format(pool, image))
self.create_disk(pool=pool, image=image, create_image=False, backstore=backstore)
self.create_disk(pool=pool, image=image, create_image=False, backstore=backstore, wwn=wwn)

def ui_command_create(self, pool=None, image=None, size=None, backstore=None, count=1):
def ui_command_create(self, pool=None, image=None, size=None, backstore=None, wwn=None,
count=1):
"""
Create a RBD image and assign to the gateway(s).
Expand All @@ -208,6 +209,7 @@ def ui_command_create(self, pool=None, image=None, size=None, backstore=None, co
size : integer, suffixed by the allocation unit - either m/M, g/G or
t/T representing the MB/GB/TB [1]
backstore : lio backstore
wwn : unit serial number
count : integer (default is 1)[2]. If the request provides a count=<n>
parameter the image name will be used as a prefix, and the count
used as a suffix to create multiple images from the same request.
Expand Down Expand Up @@ -258,7 +260,8 @@ def ui_command_create(self, pool=None, image=None, size=None, backstore=None, co
self.logger.debug("CMD: /disks/ create pool={} "
"image={} size={} "
"count={} ".format(pool, image, size, count))
self.create_disk(pool=pool, image=image, size=size, count=count, backstore=backstore)
self.create_disk(pool=pool, image=image, size=size, count=count, backstore=backstore,
wwn=wwn)

def _valid_pool(self, pool=None):
"""
Expand All @@ -283,7 +286,7 @@ def _valid_pool(self, pool=None):
return False

def create_disk(self, pool=None, image=None, size=None, count=1,
parent=None, create_image=True, backstore=None):
parent=None, create_image=True, backstore=None, wwn=None):

rc = 0

Expand All @@ -307,7 +310,7 @@ def create_disk(self, pool=None, image=None, size=None, count=1,
api_vars = {'pool': pool, 'owner': local_gw,
'count': count, 'mode': 'create',
'create_image': 'true' if create_image else 'false',
'backstore': backstore}
'backstore': backstore, 'wwn': wwn}
if size:
api_vars['size'] = size.upper()

Expand Down Expand Up @@ -1003,15 +1006,15 @@ def __init__(self, parent):
self.target_iqn = self.parent.name

def load(self, disks):
for disk in disks:
TargetDisk(self, disk)
for image_id, image in disks.items():
TargetDisk(self, image_id, image['lun_id'])

def ui_command_add(self, disk):
self.add_disk(disk)
def ui_command_add(self, disk, lun_id=None):
self.add_disk(disk, lun_id)

def add_disk(self, disk, success_msg='ok'):
def add_disk(self, disk, lun_id, success_msg='ok'):
rc = 0
api_vars = {"disk": disk}
api_vars = {"disk": disk, "lun_id": lun_id}
targetdisk_api = ('{}://localhost:{}/api/'
'targetlun/{}'.format(self.http_mode,
settings.config.api_port,
Expand All @@ -1026,7 +1029,9 @@ def add_disk(self, disk, success_msg='ok'):
disk.owner = owner
self.logger.debug("- Disk '{}' owner updated to {}"
.format(disk.image_id, owner))
TargetDisk(self, disk.image_id)
target_config = config['targets'][self.target_iqn]
lun_id = target_config['disks'][disk.image_id]['lun_id']
TargetDisk(self, disk.image_id, lun_id)
self.logger.debug("- TargetDisk '{}' added".format(disk.image_id))
if success_msg:
self.logger.info(success_msg)
Expand Down Expand Up @@ -1075,11 +1080,12 @@ class TargetDisk(UINode):

display_attributes = ['name', 'owner']

def __init__(self, parent, name):
def __init__(self, parent, name, lun_id):
UINode.__init__(self, name, parent)
ui_root = self.get_ui_root()
disk = ui_root.disks.disk_lookup[name]
self.owner = disk.owner
self.lun_id = lun_id

def summary(self):
return "Owner: {}".format(self.owner), True
return "Owner: {}, Lun: {}".format(self.owner, self.lun_id), True
54 changes: 42 additions & 12 deletions rbd-target-api.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,7 @@ def gateway(target_iqn=None, gateway_name=None):
default: FALSE
:param skipchecks: (bool) whether to skip OS/software versions checks
default: FALSE
:param force: (bool) if True will force removal of gateway.
**RESTRICTED**
Examples:
curl --insecure --user admin:admin -d ip_address=192.168.122.69
Expand Down Expand Up @@ -657,10 +658,21 @@ def gateway(target_iqn=None, gateway_name=None):
if first_gateway:
gateways = ['localhost']
elif request.method == 'DELETE':
# Update the deleted gw first, so the other gws see the updated
# portal list
gateways.remove(gateway_name)
gateways.insert(0, gateway_name)

if request.form.get('force', 'false').lower() == 'true':
# The gw we want to delete is down and the user has decided to
# force the deletion, so we do the config modification locally
# then only tell the other gws to update their state.
try:
ceph_gw = CephiSCSIGateway(logger, config, gateway_name)
ceph_gw.remove_from_config(target_iqn)
except CephiSCSIError as err:
return jsonify(message="Could not update config: {}.".format(err)), 400
else:
# Update the deleted gw first, so the other gws see the updated
# portal list
gateways.insert(0, gateway_name)
else:
# Update the new gw first, so other gws see the updated gateways list.
gateways.insert(0, gateway_name)
Expand Down Expand Up @@ -789,8 +801,18 @@ def target_disk(target_iqn=None):
owner = LUN.get_owner(config.config['gateways'], target_config['portals'])
logger.debug("{} owner will be {}".format(disk, owner))

lun_id = request.form.get('lun_id')
if lun_id is not None:
try:
lun_id_int = int(lun_id)
except ValueError:
return jsonify(message="Lun id must be a number"), 400
for target_disk in target_config['disks'].values():
if lun_id_int == target_disk['lun_id']:
return jsonify(message="Lun id {} already in use".format(lun_id)), 400
api_vars = {
'disk': disk,
'lun_id': lun_id,
'owner': owner,
'allocating_host': local_gw
}
Expand Down Expand Up @@ -883,9 +905,11 @@ def _target_disk(target_iqn=None):
logger.error("Error initializing the LUN : "
"{}".format(lun.error_msg))
return jsonify(message="Error establishing LUN instance"), 500

lun_id = request.form.get('lun_id')
if lun_id is not None:
lun_id = int(lun_id)
try:
lun.map_lun(gateway, owner, disk)
lun.map_lun(gateway, owner, disk, lun_id)
except CephiSCSIError as err:
status_code = 400 if str(err) else 500
logger.error("LUN add failed : {}".format(err))
Expand Down Expand Up @@ -966,6 +990,7 @@ def disk(pool, image):
:param preserve_image: (bool) do NOT delete RBD image
:param create_image: (bool) create RBD image if not exists
:param backstore: (str) lio backstore
:param wwn: (str) unit serial number
**RESTRICTED**
Examples:
curl --insecure --user admin:admin -d mode=create -d size=1g -d pool=rbd -d count=5
Expand Down Expand Up @@ -1026,9 +1051,11 @@ def disk(pool, image):
"{}".format(err)), 500
logger.debug("{} controls {}".format(mode, controls))

wwn = request.form.get('wwn')
disk_usable = LUN.valid_disk(config, logger, pool=pool,
image=image, size=size, mode=mode,
count=count, controls=controls, backstore=backstore)
count=count, controls=controls,
backstore=backstore, wwn=wwn)
if disk_usable != 'ok':
return jsonify(message=disk_usable), 400

Expand Down Expand Up @@ -1065,7 +1092,8 @@ def disk(pool, image):
'size': size,
'owner': local_gw,
'mode': mode,
'backstore': backstore}
'backstore': backstore,
'wwn': wwn}
if 'controls' in request.form:
api_vars['controls'] = request.form['controls']

Expand Down Expand Up @@ -1177,7 +1205,7 @@ def _disk(pool, image):
" : {}".format(lun.error_msg))
return jsonify(message="Unable to establish LUN instance"), 500

lun.allocate(False)
lun.allocate(False, request.form.get('wwn'))
if lun.error:
logger.error("LUN alloc problem - {}".format(lun.error_msg))
return jsonify(message="LUN allocation failure"), 500
Expand Down Expand Up @@ -2482,7 +2510,8 @@ def get_settings():
'default_backstore': LUN.DEFAULT_BACKSTORE,
'config': {
'minimum_gateways': settings.config.minimum_gateways
}
},
'api_version': 1
}), 200


Expand Down Expand Up @@ -2702,10 +2731,10 @@ def pre_reqs_errors():
"""

dist_translations = {
"centos": "redhat",
"centos": "rhel",
"opensuse-leap": "suse"}
valid_dists = {
"redhat": 7.4,
"rhel": 7.4,
"suse": 15.1,
"debian": 10}

Expand All @@ -2717,8 +2746,9 @@ def pre_reqs_errors():

dist = dist.lower()
dist = dist_translations.get(dist, dist)

if dist in valid_dists:
if dist == 'redhat':
if dist == 'rhel':
import platform
_, rel, _ = platform.linux_distribution(full_distribution_name=0)
# CentOS formats a release similar 7.4.1708
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def run(self):

setup(
name="ceph_iscsi",
version="3.3",
version="3.4",
description="Common classes/functions and CLI tools used to configure iSCSI "
"gateways backed by Ceph RBD",
long_description=long_description,
Expand Down