def __init__(self, mount_type, root_helper, execute=putils.execute, *args, **kwargs): self._mount_type = mount_type if mount_type == "nfs": self._mount_base = kwargs.get('nfs_mount_point_base', None) if not self._mount_base: raise exception.InvalidParameterValue( err=_('nfs_mount_point_base required')) self._mount_options = kwargs.get('nfs_mount_options', None) self._check_nfs_options() elif mount_type == "cifs": self._mount_base = kwargs.get('smbfs_mount_point_base', None) if not self._mount_base: raise exception.InvalidParameterValue( err=_('smbfs_mount_point_base required')) self._mount_options = kwargs.get('smbfs_mount_options', None) elif mount_type == "glusterfs": self._mount_base = kwargs.get('glusterfs_mount_point_base', None) if not self._mount_base: raise exception.InvalidParameterValue( err=_('glusterfs_mount_point_base required')) self._mount_options = None else: raise exception.ProtocolNotSupported(protocol=mount_type) self.root_helper = root_helper self.set_execute(execute)
def _load_extensions(self): """Load extensions specified on the command line.""" extensions = list(self.cls_list) # NOTE(thingee): Backwards compat for the old extension loader path. # We can drop this post-grizzly in the H release. old_contrib_path = ('brick.api.openstack.volume.contrib.' 'standard_extensions') new_contrib_path = 'brick.api.contrib.standard_extensions' if old_contrib_path in extensions: LOG.warn(_('osapi_volume_extension is set to deprecated path: %s'), old_contrib_path) LOG.warn(_('Please set your flag or brick.conf settings for ' 'osapi_volume_extension to: %s'), new_contrib_path) extensions = [e.replace(old_contrib_path, new_contrib_path) for e in extensions] for ext_factory in extensions: try: self.load_extension(ext_factory) except Exception as exc: LOG.warn(_('Failed to load extension %(ext_factory)s: ' '%(exc)s'), {'ext_factory': ext_factory, 'exc': exc})
def create_lv_snapshot(self, name, source_lv_name, lv_type='default'): """Creates a snapshot of a logical volume. :param name: Name to assign to new snapshot :param source_lv_name: Name of Logical Volume to snapshot :param lv_type: Type of LV (default or thin) """ source_lvref = self.get_volume(source_lv_name) if source_lvref is None: LOG.error(_("Trying to create snapshot by non-existent LV: %s") % source_lv_name) raise exception.VolumeDeviceNotFound(device=source_lv_name) cmd = ['lvcreate', '--name', name, '--snapshot', '%s/%s' % (self.vg_name, source_lv_name)] if lv_type != 'thin': size = source_lvref['size'] cmd.extend(['-L', '%sg' % (size)]) try: self._execute(*cmd, root_helper=self._root_helper, run_as_root=True) except putils.ProcessExecutionError as err: LOG.exception(_('Error creating snapshot')) LOG.error(_('Cmd :%s') % err.cmd) LOG.error(_('StdOut :%s') % err.stdout) LOG.error(_('StdErr :%s') % err.stderr) raise
def _error(self, inner, req): if not isinstance(inner, exception.QuotaError): LOG.exception(_("Caught error: %s"), unicode(inner)) safe = getattr(inner, 'safe', False) headers = getattr(inner, 'headers', None) status = getattr(inner, 'code', 500) if status is None: status = 500 msg_dict = dict(url=req.url, status=status) LOG.info(_("%(url)s returned with HTTP %(status)d") % msg_dict) outer = self.status_to_type(status) if headers: outer.headers = headers # NOTE(johannes): We leave the explanation empty here on # purpose. It could possibly have sensitive information # that should not be returned back to the user. See # bugs 868360 and 874472 # NOTE(eglynn): However, it would be over-conservative and # inconsistent with the EC2 API to hide every exception, # including those that are safe to expose, see bug 1021373 if safe: msg = (inner.msg if isinstance(inner, exception.BrickException) else unicode(inner)) params = {'exception': inner.__class__.__name__, 'explanation': msg} outer.explanation = _('%(exception)s: %(explanation)s') % params return wsgi.Fault(outer)
def activate_lv(self, name, is_snapshot=False): """Ensure that logical volume/snapshot logical volume is activated. :param name: Name of LV to activate :raises: putils.ProcessExecutionError """ # This is a no-op if requested for a snapshot on a version # of LVM that doesn't support snapshot activation. # (Assume snapshot LV is always active.) if is_snapshot and not self.supports_snapshot_lv_activation: return lv_path = self.vg_name + '/' + self._mangle_lv_name(name) # Must pass --yes to activate both the snap LV and its origin LV. # Otherwise lvchange asks if you would like to do this interactively, # and fails. cmd = ['lvchange', '-a', 'y', '--yes'] if self.supports_lvchange_ignoreskipactivation: cmd.append('-K') cmd.append(lv_path) try: self._execute(*cmd, root_helper=self._root_helper, run_as_root=True) except putils.ProcessExecutionError as err: LOG.exception(_('Error activating LV')) LOG.error(_('Cmd :%s') % err.cmd) LOG.error(_('StdOut :%s') % err.stdout) LOG.error(_('StdErr :%s') % err.stderr) raise
def __init__(self, vg_name, root_helper, create_vg=False, physical_volumes=None, lvm_type='default', executor=putils.execute): """Initialize the LVM object. The LVM object is based on an LVM VolumeGroup, one instantiation for each VolumeGroup you have/use. :param vg_name: Name of existing VG or VG to create :param root_helper: Execution root_helper method to use :param create_vg: Indicates the VG doesn't exist and we want to create it :param physical_volumes: List of PVs to build VG on :param lvm_type: VG and Volume type (default, or thin) :param executor: Execute method to use, None uses common/processutils """ super(LVM, self).__init__(execute=executor, root_helper=root_helper) self.vg_name = vg_name self.pv_list = [] self.lv_list = [] self.vg_size = 0.0 self.vg_free_space = 0.0 self.vg_lv_count = 0 self.vg_uuid = None self.vg_thin_pool = None self.vg_thin_pool_size = 0.0 self.vg_thin_pool_free_space = 0.0 self._supports_snapshot_lv_activation = None self._supports_lvchange_ignoreskipactivation = None if create_vg and physical_volumes is not None: self.pv_list = physical_volumes try: self._create_vg(physical_volumes) except putils.ProcessExecutionError as err: LOG.exception(_('Error creating Volume Group')) LOG.error(_('Cmd :%s') % err.cmd) LOG.error(_('StdOut :%s') % err.stdout) LOG.error(_('StdErr :%s') % err.stderr) raise exception.VolumeGroupCreationFailed(vg_name=self.vg_name) if self._vg_exists() is False: LOG.error(_('Unable to locate Volume Group %s') % vg_name) raise exception.VolumeGroupNotFound(vg_name=vg_name) # NOTE: we assume that the VG has been activated outside if Brick if lvm_type == 'thin': pool_name = "%s-pool" % self.vg_name if self.get_volume(pool_name) is None: self.create_thin_pool(pool_name) else: self.vg_thin_pool = pool_name self.activate_lv(self.vg_thin_pool) self.pv_list = self.get_all_physical_volumes(root_helper, vg_name)
def rename_volume(self, lv_name, new_name): """Change the name of an existing volume.""" try: self._execute('lvrename', self.vg_name, lv_name, new_name, root_helper=self._root_helper, run_as_root=True) except putils.ProcessExecutionError as err: LOG.exception(_('Error renaming logical volume')) LOG.error(_('Cmd :%s') % err.cmd) LOG.error(_('StdOut :%s') % err.stdout) LOG.error(_('StdErr :%s') % err.stderr) raise
def extend_volume(self, lv_name, new_size): """Extend the size of an existing volume.""" try: self._execute('lvextend', '-L', new_size, '%s/%s' % (self.vg_name, lv_name), root_helper=self._root_helper, run_as_root=True) except putils.ProcessExecutionError as err: LOG.exception(_('Error extending Volume')) LOG.error(_('Cmd :%s') % err.cmd) LOG.error(_('StdOut :%s') % err.stdout) LOG.error(_('StdErr :%s') % err.stderr) raise
def remove_iscsi_target(self, tid, lun, vol_id, vol_name, **kwargs): LOG.info(_('Removing iscsi_target: %s') % vol_id) vol_uuid_name = vol_name iqn = '%s%s' % (self.iscsi_target_prefix, vol_uuid_name) try: self._execute('brick-rtstool', 'delete', iqn, run_as_root=True) except putils.ProcessExecutionError as e: LOG.error(_("Failed to remove iscsi target for volume " "id:%s.") % vol_id) LOG.error("%s" % e) raise exception.ISCSITargetRemoveFailed(volume_id=vol_id)
def action_peek_json(body): """Determine action to invoke.""" try: decoded = jsonutils.loads(body) except ValueError: msg = _("cannot understand JSON") raise exception.MalformedRequestBody(reason=msg) # Make sure there's exactly one key... if len(decoded) != 1: msg = _("too many body keys") raise exception.MalformedRequestBody(reason=msg) # Return the action and the decoded body... return decoded.keys()[0]
def flush_multipath_devices(self): try: self._execute('multipath', '-F', run_as_root=True, root_helper=self._root_helper) except putils.ProcessExecutionError as exc: LOG.warn(_("multipath call failed exit (%(code)s)") % {'code': exc.exit_code})
def read(self, i=None): result = self.data.read(i) self.bytes_read += len(result) if self.bytes_read > self.limit: msg = _("Request is too large.") raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg) return result
def update_volume_group_info(self): """Update VG info for this instantiation. Used to update member fields of object and provide a dict of info for caller. :returns: Dictionaries of VG info """ vg_list = self.get_all_volume_groups(self._root_helper, self.vg_name) if len(vg_list) != 1: LOG.error(_('Unable to find VG: %s') % self.vg_name) raise exception.VolumeGroupNotFound(vg_name=self.vg_name) self.vg_size = float(vg_list[0]['size']) self.vg_free_space = float(vg_list[0]['available']) self.vg_lv_count = int(vg_list[0]['lv_count']) self.vg_uuid = vg_list[0]['uuid'] if self.vg_thin_pool is not None: for lv in self.get_all_volumes(self._root_helper, self.vg_name): if lv['name'] == self.vg_thin_pool: self.vg_thin_pool_size = lv['size'] tpfs = self._get_thin_pool_free_space(self.vg_name, self.vg_thin_pool) self.vg_thin_pool_free_space = tpfs
def check_exclusive_options(**kwargs): """Checks that only one of the provided options is actually not-none. Iterates over all the kwargs passed in and checks that only one of said arguments is not-none, if more than one is not-none then an exception will be raised with the names of those arguments who were not-none. """ if not kwargs: return pretty_keys = kwargs.pop("pretty_keys", True) exclusive_options = {} for (k, v) in kwargs.iteritems(): if v is not None: exclusive_options[k] = True if len(exclusive_options) > 1: # Change the format of the names from pythonic to # something that is more readable. # # Ex: 'the_key' -> 'the key' if pretty_keys: names = [k.replace('_', ' ') for k in kwargs.keys()] else: names = kwargs.keys() names = ", ".join(sorted(names)) msg = (_("May specify only one of %s") % (names)) raise exception.InvalidInput(reason=msg)
def __init__(self, message=None, **kwargs): self.kwargs = kwargs if 'code' not in self.kwargs: try: self.kwargs['code'] = self.code except AttributeError: pass if not message: try: message = self.message % kwargs except Exception: # kwargs doesn't match a variable in the message # log the issue and the kwargs msg = (_("Exception in string format operation. msg='%s'") % self.message) LOG.exception(msg) for name, value in kwargs.iteritems(): LOG.error("%s: %s" % (name, value)) # at least get the core message out if something happened message = self.message # Put the message in 'msg' so that we can access it. If we have it in # message it will be overshadowed by the class' message attribute self.msg = message super(BrickException, self).__init__(message)
def __init__(self, message=None, **kwargs): self.kwargs = kwargs if 'code' not in self.kwargs: try: self.kwargs['code'] = self.code except AttributeError: pass for k, v in self.kwargs.iteritems(): if isinstance(v, Exception): self.kwargs[k] = six.text_type(v) if not message: try: message = self.message % kwargs except Exception: exc_info = sys.exc_info() # kwargs doesn't match a variable in the message # log the issue and the kwargs LOG.exception(_("Exception in string format operation.")) for name, value in kwargs.iteritems(): LOG.error("%s: %s" % (name, value)) if CONF.fatal_exception_format_errors: raise exc_info[0], exc_info[1], exc_info[2] # at least get the core message out if something happened message = self.message elif isinstance(message, Exception): message = six.text_type(message) # Put the message in 'msg' so that we can access it. If we have it in # message it will be overshadowed by the class' message attribute self.msg = message super(BrickException, self).__init__(message)
def __init__(self, name, loader=None): """Initialize, but do not start the WSGI server. :param name: The name of the WSGI server given to the loader. :param loader: Loads the WSGI application using the given name. :returns: None """ self.name = name self.manager = self._get_manager() self.loader = loader or wsgi.Loader() self.app = self.loader.load_app(name) self.host = getattr(CONF, '%s_listen' % name, "0.0.0.0") self.port = getattr(CONF, '%s_listen_port' % name, 0) self.workers = getattr(CONF, '%s_workers' % name, processutils.get_worker_count()) if self.workers < 1: LOG.warn(_("Value of config option %(name)s_workers must be " "integer greater than 1. Input value ignored.") % {'name': name}) # Reset workers to default self.workers = processutils.get_worker_count() self.server = wsgi.Server(name, self.app, host=self.host, port=self.port)
def create_iscsi_target(self, name, tid, lun, path, chap_auth=None, **kwargs): # NOTE (jdg): Address bug: 1175207 kwargs.pop('old_name', None) self._new_target(name, tid, **kwargs) self._new_logicalunit(tid, lun, path, **kwargs) if chap_auth is not None: (type, username, password) = chap_auth.split() self._new_auth(tid, type, username, password, **kwargs) conf_file = self.iet_conf if os.path.exists(conf_file): try: volume_conf = """ Target %s %s Lun 0 Path=%s,Type=%s """ % (name, chap_auth, path, self._iotype(path)) with utils.temporary_chown(conf_file): f = open(conf_file, 'a+') f.write(volume_conf) f.close() except putils.ProcessExecutionError as e: vol_id = name.split(':')[1] LOG.error(_("Failed to create iscsi target for volume " "id:%(vol_id)s: %(e)s") % {'vol_id': vol_id, 'e': e}) raise exception.ISCSITargetCreateFailed(volume_id=vol_id) return tid
def __call__(self, req): """Represents a single call through this middleware. We should record the request if we have a limit relevant to it. If no limit is relevant to the request, ignore it. If the request should be rate limited, return a fault telling the user they are over the limit and need to retry later. """ verb = req.method url = req.url context = req.environ.get("brick.context") if context: username = context.user_id else: username = None delay, error = self._limiter.check_for_delay(verb, url, username) if delay: msg = _("This request was rate-limited.") retry = time.time() + delay return wsgi.OverLimitFault(msg, error, retry) req.environ["brick.limits"] = self._limiter.get_limits(username) return self.application
def __init__(self, mount_type, root_helper, driver=None, execute=putils.execute, device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT, *args, **kwargs): kwargs = kwargs or {} conn = kwargs.get('conn') if conn: mount_point_base = conn.get('mount_point_base') if mount_type.lower() == 'nfs': kwargs['nfs_mount_point_base'] =\ kwargs.get('nfs_mount_point_base') or\ mount_point_base elif mount_type.lower() == 'glusterfs': kwargs['glusterfs_mount_point_base'] =\ kwargs.get('glusterfs_mount_point_base') or\ mount_point_base else: LOG.warn(_("Connection details not present." " RemoteFsClient may not initialize properly.")) self._remotefsclient = remotefs.RemoteFsClient(mount_type, root_helper, execute=execute, *args, **kwargs) super(RemoteFsConnector, self).__init__(root_helper, driver=driver, execute=execute, device_scan_attempts= device_scan_attempts, *args, **kwargs)
def remove_iscsi_target(self, tid, lun, vol_id, vol_name, **kwargs): LOG.info(_('Removing iscsi_target for volume: %s') % vol_id) self._delete_logicalunit(tid, lun, **kwargs) self._delete_target(tid, **kwargs) vol_uuid_file = vol_name conf_file = self.iet_conf if os.path.exists(conf_file): with utils.temporary_chown(conf_file): try: iet_conf_text = open(conf_file, 'r+') full_txt = iet_conf_text.readlines() new_iet_conf_txt = [] count = 0 for line in full_txt: if count > 0: count -= 1 continue elif re.search(vol_uuid_file, line): count = 2 continue else: new_iet_conf_txt.append(line) iet_conf_text.seek(0) iet_conf_text.truncate(0) iet_conf_text.writelines(new_iet_conf_txt) finally: iet_conf_text.close()
def __init__(self, verb, uri, regex, value, unit): """Initialize a new `Limit`. @param verb: HTTP verb (POST, PUT, etc.) @param uri: Human-readable URI @param regex: Regular expression format for this limit @param value: Integer number of requests which can be made @param unit: Unit of measure for the value parameter """ self.verb = verb self.uri = uri self.regex = regex self.value = int(value) self.unit = unit self.unit_string = self.display_unit().lower() self.remaining = int(value) if value <= 0: raise ValueError("Limit value must be > 0") self.last_request = None self.next_request = None self.water_level = 0 self.capacity = self.unit self.request_value = float(self.capacity) / float(self.value) msg = _("Only %(value)s %(verb)s request(s) can be " "made to %(uri)s every %(unit_string)s.") self.error_message = msg % self.__dict__
def __call__(self, environ, start_response): r"""Subclasses will probably want to implement __call__ like this: @webob.dec.wsgify(RequestClass=Request) def __call__(self, req): # Any of the following objects work as responses: # Option 1: simple string res = 'message\n' # Option 2: a nicely formatted HTTP exception page res = exc.HTTPForbidden(explanation='Nice try') # Option 3: a webob Response object (in case you need to play with # headers, or you want to be treated like an iterable) res = Response(); res.app_iter = open('somefile') # Option 4: any wsgi app to be run next res = self.application # Option 5: you can get a Response object for a wsgi app, too, to # play with headers etc res = req.get_response(self.application) # You can then just return your response... return res # ... or set req.response and return None. req.response = res See the end of http://pythonpaste.org/webob/modules/dec.html for more info. """ raise NotImplementedError(_('You must implement __call__'))
def attach(self, *slaves): """Attach one or more slave templates. Attaches one or more slave templates to the master template. Slave templates must have a root element with the same tag as the master template. The slave template's apply() method will be called to determine if the slave should be applied to this master; if it returns False, that slave will be skipped. (This allows filtering of slaves based on the version of the master template.) """ slave_list = [] for slave in slaves: slave = slave.wrap() # Make sure we have a tree match if slave.root.tag != self.root.tag: msg = (_("Template tree mismatch; adding slave %(slavetag)s " "to master %(mastertag)s") % {'slavetag': slave.root.tag, 'mastertag': self.root.tag}) raise ValueError(msg) # Make sure slave applies to this template if not slave.apply(self): continue slave_list.append(slave) # Add the slaves self.slaves.extend(slave_list)
def get_fc_hbas(self): """Get the Fibre Channel HBA information.""" out = None try: out, err = self._execute('systool', '-c', 'fc_host', '-v', run_as_root=True, root_helper=self._root_helper) except putils.ProcessExecutionError as exc: # This handles the case where rootwrap is used # and systool is not installed # 96 = nova.cmd.rootwrap.RC_NOEXECFOUND: if exc.exit_code == 96: LOG.warn(_("systool is not installed")) return [] except OSError as exc: # This handles the case where rootwrap is NOT used # and systool is not installed if exc.errno == errno.ENOENT: LOG.warn(_("systool is not installed")) return [] # No FC HBAs were found if out is None: return [] lines = out.split('\n') # ignore the first 2 lines lines = lines[2:] hbas = [] hba = {} lastline = None for line in lines: line = line.strip() # 2 newlines denotes a new hba port if line == '' and lastline == '': if len(hba) > 0: hbas.append(hba) hba = {} else: val = line.split('=') if len(val) == 2: key = val[0].strip().replace(" ", "") value = val[1].strip() hba[key] = value.replace('"', '') lastline = line return hbas
def __iter__(self): for chunk in self.data: self.bytes_read += len(chunk) if self.bytes_read > self.limit: msg = _("Request is too large.") raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg) else: yield chunk
def show_target(self, tid, iqn=None, **kwargs): if iqn is None: raise exception.InvalidParameterValue( err=_('valid iqn needed for show_target')) tid = self._get_target(iqn) if tid is None: raise exception.NotFound()
def flush_multipath_device(self, device): try: LOG.debug("Flush multipath device %s" % device) self._execute('multipath', '-f', device, run_as_root=True, root_helper=self._root_helper) except putils.ProcessExecutionError as exc: LOG.warn(_("multipath call failed exit (%(code)s)") % {'code': exc.exit_code})
def construct(self): """Construct a template. Called to construct a template instance, which it must return. Only called once. """ raise NotImplementedError(_("subclasses must implement construct()!"))
def start(self, interval, initial_delay=None): while not self._stop: try: self.f(*self.args, **self.kw) except loopingcall.LoopingCallDone: return self except Exception: LOG.exception(_('in fixed duration looping call')) raise
class ProtocolNotSupported(BrickException): message = _("Connect to volume via protocol %(protocol)s not supported.")
def _set_read_deleted(self, read_deleted): if read_deleted not in ('no', 'yes', 'only'): raise ValueError( _("read_deleted can only be one of 'no', " "'yes' or 'only', not %r") % read_deleted) self._read_deleted = read_deleted
def __init__(self, application): LOG.warn(_('brick is deprecated. Please ' 'use brick instead.')) # Avoid circular imports from here. Can I just remove this class? from brick.api.middleware import fault super(FaultWrapper, self).__init__(fault.FaultWrapper(application))
class PasteAppNotFound(NotFound): message = _("Could not load paste app '%(name)s' from %(path)s")
class InvalidParameterValue(Invalid): message = _("%(err)s")
class NoFibreChannelVolumeDeviceFound(BrickException): message = _("Unable to find a Fibre Channel volume device.")
class NoFibreChannelHostsFound(BrickException): message = _("We are unable to locate any Fibre Channel devices.")
class VolumeGroupNotFound(BrickException): message = _('Unable to find Volume Group: %(vg_name)s')
class VolumeDeviceNotFound(BrickException): message = _("Volume device not found at %(device)s.")
class ISCSITargetCreateFailed(BrickException): message = _("Failed to create iscsi target for volume %(volume_id)s.")
class VolumeGroupCreationFailed(BrickException): message = _('Failed to create Volume Group: %(vg_name)s')
class ISCSITargetRemoveFailed(BrickException): message = _("Failed to remove iscsi target for volume %(volume_id)s.")
class ISCSITargetAttachFailed(BrickException): message = _("Failed to attach iSCSI target for volume %(volume_id)s.")
def __init__(self, ext_mgr=None): LOG.warn(_('brick.api.openstack.volume:APIRouter is deprecated. ' 'Please use brick.api.v1.router:APIRouter instead.')) super(APIRouter, self).__init__(ext_mgr)
class Invalid(BrickException): message = _("Unacceptable parameters.") code = 400
def index(self, req): LOG.info(_("API get_Connector called")) connector = self.manager.get_connector() LOG.debug("Connector: %s" % connector) return {"connector": connector}
class NotFound(BrickException): message = _("Resource could not be found.") code = 404 safe = True
def find_multipath_device(self, device): """Find a multipath device associated with a LUN device name. device can be either a /dev/sdX entry or a multipath id. """ mdev = None devices = [] out = None try: (out, err) = self._execute('multipath', '-l', device, run_as_root=True, root_helper=self._root_helper) except putils.ProcessExecutionError as exc: LOG.warn( _("multipath call failed exit (%(code)s)") % {'code': exc.exit_code}) return None if out: lines = out.strip() lines = lines.split("\n") if lines: line = lines[0] info = line.split(" ") # device line output is different depending # on /etc/multipath.conf settings. if info[1][:2] == "dm": mdev = "/dev/%s" % info[1] mdev_id = info[0] elif info[2][:2] == "dm": mdev = "/dev/%s" % info[2] mdev_id = info[1].replace('(', '') mdev_id = mdev_id.replace(')', '') if mdev is None: LOG.warn( _("Couldn't find multipath device %(line)s") % {'line': line}) return None LOG.debug("Found multipath device = %(mdev)s" % {'mdev': mdev}) device_lines = lines[3:] for dev_line in device_lines: if dev_line.find("policy") != -1: continue dev_line = dev_line.lstrip(' |-`') dev_info = dev_line.split() address = dev_info[0].split(":") dev = { 'device': '/dev/%s' % dev_info[1], 'host': address[0], 'channel': address[1], 'id': address[2], 'lun': address[3] } devices.append(dev) if mdev is not None: info = {"device": mdev, "id": mdev_id, "devices": devices} return info return None