Ejemplo n.º 1
0
    def create_disk(self, pool=None, image=None, size=None, parent=None):

        if not parent:
            parent = self

        local_gw = this_host()
        disk_key = "{}.{}".format(pool, image)

        if not self._valid_pool(pool):
            return

        self.logger.debug("Creating/mapping disk {}/{}".format(pool, image))

        # make call to local api server's all_ method
        disk_api = '{}://127.0.0.1:{}/api/all_disk/{}'.format(
            self.http_mode, settings.config.api_port, disk_key)

        api_vars = {
            'pool': pool,
            'size': size.upper(),
            'owner': local_gw,
            'mode': 'create'
        }

        self.logger.debug("Issuing disk create request")

        api = APIRequest(disk_api, data=api_vars)
        api.put()

        if api.response.status_code == 200:
            # rbd create and map successful across all gateways so request
            # it's details and add to the UI
            self.logger.debug("- LUN is ready on all gateways")

            ceph_pools = self.parent.ceph.local_ceph.pools
            ceph_pools.refresh()

            self.logger.debug("Updating UI for the new disk")
            disk_api = disk_api.replace('/all_disk/', '/disk/')
            api = APIRequest(disk_api)
            api.get()

            if api.response.status_code == 200:
                image_config = api.response.json()
                Disk(parent, disk_key, image_config)
                self.logger.info('ok')
            else:
                raise GatewayAPIError(
                    "Unable to retrieve disk details from the API")

        else:
            self.logger.error("Failed : {}".format(
                api.response.json()['message']))
Ejemplo n.º 2
0
    def ui_command_create(self, target_iqn):
        """
        Create an iSCSI target. This target is defined across all gateway nodes,
        providing the client with a single 'image' for iscsi discovery.

        Only ONE iSCSI target is supported, at this time.
        """

        self.logger.debug("CMD: /iscsi create {}".format(target_iqn))

        defined_targets = [tgt.name for tgt in self.children]
        if len(defined_targets) > 0:
            self.logger.error("Only ONE iscsi target image is supported")
            return

        # We need LIO to be empty, so check there aren't any targets defined
        local_lio = root.RTSRoot()
        current_target_names = [tgt.wwn for tgt in local_lio.targets]
        if current_target_names:
            self.logger.error("Local LIO instance already has LIO configured "
                              "with a target - unable to continue")
            return

        # OK - this request is valid, but is the IQN usable?
        if not valid_iqn(target_iqn):
            self.logger.error("IQN name '{}' is not valid for "
                              "iSCSI".format(target_iqn))
            return


        # 'safe' to continue with the definition
        self.logger.debug("Create an iscsi target definition in the UI")

        local_api = ('{}://127.0.0.1:{}/api/'
                     'target/{}'.format(self.http_mode,
                                        settings.config.api_port,
                                        target_iqn))

        api = APIRequest(local_api)
        api.put()

        if api.response.status_code == 200:
            self.logger.info('ok')
            # create the target entry in the UI tree
            Target(target_iqn, self)
        else:
            self.logger.error("Failed to create the target on the local node")

            raise GatewayAPIError("iSCSI target creation failed - "
                                  "{}".format(response_message(api.response,
                                                               self.logger)))
Ejemplo n.º 3
0
    def ui_command_create(self, target_iqn):
        """
        Create an iSCSI target. This target is defined across all gateway nodes,
        providing the client with a single 'image' for iscsi discovery.
        """

        self.logger.debug("CMD: /iscsi create {}".format(target_iqn))

        # is the IQN usable?
        try:
            target_iqn, iqn_type = normalize_wwn(['iqn'], target_iqn)
        except RTSLibError:
            self.logger.error("IQN name '{}' is not valid for "
                              "iSCSI".format(target_iqn))
            return

        # 'safe' to continue with the definition
        self.logger.debug("Create an iscsi target definition in the UI")

        local_api = ('{}://localhost:{}/api/'
                     'target/{}'.format(self.http_mode,
                                        settings.config.api_port, target_iqn))

        api = APIRequest(local_api)
        api.put()

        if api.response.status_code == 200:
            self.logger.info('ok')
            # create the target entry in the UI tree
            target_exists = len([
                target for target in self.children if target.name == target_iqn
            ]) > 0
            if not target_exists:
                Target(target_iqn, self)
        else:
            self.logger.error("Failed to create the target on the local node")

            raise GatewayAPIError("iSCSI target creation failed - "
                                  "{}".format(
                                      response_message(api.response,
                                                       self.logger)))
Ejemplo n.º 4
0
    def create_disk(self,
                    pool=None,
                    image=None,
                    size=None,
                    count=1,
                    parent=None,
                    create_image=True,
                    backstore=None):

        rc = 0

        if not parent:
            parent = self

        local_gw = this_host()

        disk_key = "{}/{}".format(pool, image)

        if not self._valid_pool(pool):
            return

        self.logger.debug("Creating/mapping disk {}/{}".format(pool, image))

        # make call to local api server's disk endpoint
        disk_api = '{}://localhost:{}/api/disk/{}'.format(
            self.http_mode, settings.config.api_port, disk_key)
        api_vars = {
            'pool': pool,
            'owner': local_gw,
            'count': count,
            'mode': 'create',
            'create_image': 'true' if create_image else 'false',
            'backstore': backstore
        }
        if size:
            api_vars['size'] = size.upper()

        self.logger.debug("Issuing disk create request")

        api = APIRequest(disk_api, data=api_vars)
        api.put()

        if api.response.status_code == 200:
            # rbd create and map successful across all gateways so request
            # it's details and add to the UI
            self.logger.debug("- LUN(s) ready on all gateways")
            self.logger.info("ok")

            self.logger.debug("Updating UI for the new disk(s)")
            for n in range(1, (int(count) + 1), 1):

                if int(count) > 1:
                    disk_key = "{}/{}{}".format(pool, image, n)
                else:
                    disk_key = "{}/{}".format(pool, image)

                disk_api = ('{}://localhost:{}/api/disk/'
                            '{}'.format(self.http_mode,
                                        settings.config.api_port, disk_key))

                api = APIRequest(disk_api)
                api.get()

                if api.response.status_code == 200:
                    try:
                        image_config = api.response.json()
                    except Exception:
                        raise GatewayAPIError("Malformed REST API response")

                    disk_pool = None
                    for current_disk_pool in self.children:
                        if current_disk_pool.name == pool:
                            disk_pool = current_disk_pool
                            break
                    if disk_pool:
                        Disk(disk_pool, disk_key, image_config)
                    else:
                        DiskPool(parent, pool, [image_config])
                    self.logger.debug("{} added to the UI".format(disk_key))
                else:
                    raise GatewayAPIError(
                        "Unable to retrieve disk details "
                        "for '{}' from the API".format(disk_key))

            ceph_pools = self.parent.ceph.cluster.pools
            ceph_pools.refresh()

        else:
            self.logger.error("Failed : {}".format(
                response_message(api.response, self.logger)))
            rc = 8

        return rc
Ejemplo n.º 5
0
    def create_disk(self,
                    pool=None,
                    image=None,
                    size=None,
                    count=1,
                    parent=None):

        rc = 0

        if not parent:
            parent = self

        local_gw = this_host()

        disk_key = "{}.{}".format(pool, image)

        if not self._valid_pool(pool):
            return

        self.logger.debug("Creating/mapping disk {}/{}".format(pool, image))

        # make call to local api server's disk endpoint
        disk_api = '{}://127.0.0.1:{}/api/disk/{}'.format(
            self.http_mode, settings.config.api_port, disk_key)

        api_vars = {
            'pool': pool,
            'size': size.upper(),
            'owner': local_gw,
            'count': count,
            'mode': 'create'
        }

        self.logger.debug("Issuing disk create request")

        api = APIRequest(disk_api, data=api_vars)
        api.put()

        if api.response.status_code == 200:
            # rbd create and map successful across all gateways so request
            # it's details and add to the UI
            self.logger.debug("- LUN(s) ready on all gateways")
            self.logger.info("ok")

            ceph_pools = self.parent.ceph.local_ceph.pools
            ceph_pools.refresh()

            self.logger.debug("Updating UI for the new disk(s)")
            for n in range(1, (int(count) + 1), 1):

                if count > 1:
                    disk_key = "{}.{}{}".format(pool, image, n)
                else:
                    disk_key = "{}.{}".format(pool, image)

                disk_api = ('{}://127.0.0.1:{}/api/disk/'
                            '{}'.format(self.http_mode,
                                        settings.config.api_port, disk_key))

                api = APIRequest(disk_api)
                api.get()

                if api.response.status_code == 200:
                    image_config = api.response.json()
                    Disk(parent, disk_key, image_config)
                    self.logger.debug("{} added to the UI".format(disk_key))
                else:
                    raise GatewayAPIError(
                        "Unable to retrieve disk details "
                        "for '{}' from the API".format(disk_key))
        else:
            self.logger.error("Failed : {}".format(
                api.response.json()['message']))
            rc = 8

        return rc
Ejemplo n.º 6
0
    def ui_command_create(self, pool=None, image=None, size=None):
        """
        Create a LUN and assign to the gateway.

        The create process needs the pool name, rbd image name
        and the size parameter. 'size' should be a numeric suffixed
        by either M, G or T (representing the allocation unit)
        """
        # NB the text above is shown on a help create request in the CLI

        if not self._valid_request(pool, image, size):
            return

        # get pool, image, and size ; use this host as the creator
        local_gw = this_host()
        disk_key = "{}.{}".format(pool, image)

        other_gateways = get_other_gateways(self.parent.target.children)
        if len(other_gateways) < 1:
            self.logger.error(
                "At least 2 gateways must be defined before disks can be added"
            )
            return

        self.logger.debug("Creating/mapping disk {}/{}".format(pool, image))

        # make call to local api server first!
        disk_api = '{}://127.0.0.1:{}/api/disk/{}'.format(
            self.http_mode, settings.config.api_port, disk_key)

        api_vars = {
            'pool': pool,
            'size': size.upper(),
            'owner': local_gw,
            'mode': 'create'
        }

        self.logger.debug("Processing local LIO instance")
        response = put(disk_api,
                       data=api_vars,
                       auth=(settings.config.api_user,
                             settings.config.api_password),
                       verify=settings.config.api_ssl_verify)

        if response.status_code == 200:
            # rbd create and map successful, so request it's details and add
            # to the gwcli
            self.logger.debug("- LUN is ready on local")
            response = get(disk_api,
                           auth=(settings.config.api_user,
                                 settings.config.api_password),
                           verify=settings.config.api_ssl_verify)

            if response.status_code == 200:
                image_config = response.json()
                Disk(self, disk_key, image_config)

                self.logger.debug("Processing other gateways")
                for gw in other_gateways:
                    disk_api = '{}://{}:{}/api/disk/{}'.format(
                        self.http_mode, gw, settings.config.api_port, disk_key)

                    response = put(disk_api,
                                   data=api_vars,
                                   auth=(settings.config.api_user,
                                         settings.config.api_password),
                                   verify=settings.config.api_ssl_verify)

                    if response.status_code == 200:
                        self.logger.debug("- LUN is ready on {}".format(gw))
                    else:
                        raise GatewayAPIError(response.text)

        else:
            raise GatewayLIOError(
                "- Error defining the rbd image to the local gateway")

        ceph_pools = self.parent.ceph.pools
        ceph_pools.refresh()

        self.logger.info('ok')
Ejemplo n.º 7
0
    def ui_command_resize(self, size=None):
        """
        The resize command allows you to increase the size of an
        existing rbd image. Attempting to decrease the size of an
        rbd will be ignored.

        size: new size including unit suffix e.g. 300G

        """

        # resize is actually managed by the same lun and api endpoint as
        # create so this logic is very similar to a 'create' request

        if not size:
            self.logger.error(
                "Specify a size value (current size is {})".format(
                    self.size_h))
            return

        size_rqst = size.upper()
        if not valid_size(size_rqst):
            self.logger.error(
                "Size parameter value is not valid syntax (must be of the form 100G, or 1T)"
            )
            return

        new_size = convert_2_bytes(size_rqst)
        if self.size >= new_size:
            # current size is larger, so nothing to do
            self.logger.error(
                "New size isn't larger than the current image size, ignoring request"
            )
            return

        # At this point the size request needs to be honoured
        self.logger.debug("Resizing {} to {}".format(self.image_id, size_rqst))

        local_gw = this_host()
        other_gateways = get_other_gateways(self.parent.parent.target.children)

        # make call to local api server first!
        disk_api = '{}://127.0.0.1:{}/api/disk/{}'.format(
            self.http_mode, settings.config.api_port, self.image_id)

        api_vars = {
            'pool': self.pool,
            'size': size_rqst,
            'owner': local_gw,
            'mode': 'resize'
        }

        self.logger.debug("Processing local LIO instance")
        response = put(disk_api,
                       data=api_vars,
                       auth=(settings.config.api_user,
                             settings.config.api_password),
                       verify=settings.config.api_ssl_verify)

        if response.status_code == 200:
            # rbd resize request successful, so update the local information
            self.logger.debug("- LUN resize complete")
            self.get_meta_data()

            self.logger.debug("Processing other gateways")
            for gw in other_gateways:
                disk_api = '{}://{}:{}/api/disk/{}'.format(
                    self.http_mode, gw, settings.config.api_port,
                    self.image_id)

                response = put(disk_api,
                               data=api_vars,
                               auth=(settings.config.api_user,
                                     settings.config.api_password),
                               verify=settings.config.api_ssl_verify)

                if response.status_code == 200:
                    self.logger.debug(
                        "- LUN resize registered on {}".format(gw))
                else:
                    raise GatewayAPIError(response.text)

        else:
            raise GatewayAPIError(response.text)

        self.logger.info('ok')
Ejemplo n.º 8
0
    def ui_command_delete(self, image_id):
        """
        Delete a given rbd image from the configuration and ceph. This is a
        destructive action that could lead to data loss, so please ensure
        the rbd image is correct!

        > delete <rbd_image_name>

        Also note that the delete process is a synchronous task, so the larger
        the rbd image is, the longer the delete will take to run.

        """

        # 1st does the image id given exist?
        rbd_list = [disk.name for disk in self.children]
        if image_id not in rbd_list:
            self.logger.error(
                "- the disk '{}' does not exist in this configuration".format(
                    image_id))
            return

        # Although the LUN class will check that the lun is unallocated before attempting
        # a delete, it seems cleaner and more responsive to check through the object model
        # here before sending a delete request

        disk_users = self.disk_in_use(image_id)
        if disk_users:
            self.logger.error(
                "- Unable to delete '{}', it is currently allocated to:".
                format(image_id))

            # error_str = "- Unable to delete '{}', it is currently allocated to:\n".format(image_id)
            for client in disk_users:
                self.logger.error("  - {}".format(client))
            return

        self.logger.debug("Deleting rbd {}".format(image_id))

        local_gw = this_host()
        other_gateways = get_other_gateways(self.parent.target.children)

        api_vars = {'purge_host': local_gw}
        # process other gateways first
        for gw_name in other_gateways:
            disk_api = '{}://{}:{}/api/disk/{}'.format(
                self.http_mode, gw_name, settings.config.api_port, image_id)

            self.logger.debug("- removing '{}' from {}".format(
                image_id, gw_name))
            response = delete(disk_api,
                              data=api_vars,
                              auth=(settings.config.api_user,
                                    settings.config.api_password),
                              verify=settings.config.api_ssl_verify)

            if response.status_code == 200:
                pass
            elif response.status_code == 400:
                # 400 means the rbd is still allocated to a client
                msg = json.loads(response.text)['message']
                self.logger.error(msg)
                return
            else:
                # delete failed - don't know why, pass the error to the
                # admin and abort
                raise GatewayAPIError(response.text)

        # at this point the remote gateways are cleaned up, now perform the
        # purge on the local host which will also purge the rbd
        disk_api = '{}://127.0.0.1:{}/api/disk/{}'.format(
            self.http_mode, settings.config.api_port, image_id)

        self.logger.debug(
            "- removing '{}' from the local machine".format(image_id))

        response = delete(disk_api,
                          data=api_vars,
                          auth=(settings.config.api_user,
                                settings.config.api_password),
                          verify=settings.config.api_ssl_verify)

        if response.status_code == 200:
            self.logger.debug("- rbd removed")
            disk_object = [
                disk for disk in self.children if disk.name == image_id
            ][0]
            self.remove_child(disk_object)
        else:
            raise GatewayLIOError(
                "--> Failed to remove the device from the local machine")

        ceph_pools = self.parent.ceph.pools
        ceph_pools.refresh()

        self.logger.info('ok')
Ejemplo n.º 9
0
    def ui_command_create(self, client_iqn):
        """
        Clients may be created using the 'create' sub-command. The initial
        definition will be added to each gateway without any authentication
        set, so once the client is created you must 'cd' to the client and
        add authentication (auth) and any desired disks (disk).

        > create <client_iqn>

        """

        cli_seed = {"luns": {}, "auth": {}}

        # make sure the iqn isn't already defined
        existing_clients = [client.name for client in self.children]
        if client_iqn in existing_clients:
            self.logger.error(
                "Client '{}' is already defined".format(client_iqn))
            return

        try:
            valid_iqn = normalize_wwn(['iqn'], client_iqn)
        except RTSLibError:
            self.logger.critical(
                "An iqn of '{}' is not a valid name for iSCSI".format(
                    client_iqn))
            return

        # run the create locally - to seed the config object
        other_gateways = get_other_gateways(
            self.parent.parent.parent.target.children)
        api_vars = {"committing_host": this_host()}
        client_api = '{}://127.0.0.1:{}/api/client/{}'.format(
            self.http_mode, settings.config.api_port, client_iqn)

        self.logger.debug("Client CREATE for {}".format(client_iqn))
        response = put(client_api,
                       data=api_vars,
                       auth=(settings.config.api_user,
                             settings.config.api_password),
                       verify=settings.config.api_ssl_verify)

        if response.status_code == 200:
            Client(self, client_iqn, cli_seed)
            self.logger.debug("- Client '{}' added locally".format(client_iqn))
            # defined locally OK, so let's apply to the other gateways
            for gw in other_gateways:
                client_api = '{}://{}:{}/api/client/{}'.format(
                    self.http_mode, gw, settings.config.api_port, client_iqn)

                response = put(client_api,
                               data=api_vars,
                               auth=(settings.config.api_user,
                                     settings.config.api_password),
                               verify=settings.config.api_ssl_verify)

                if response.status_code == 200:
                    self.logger.debug("- Client '{}' added to {}".format(
                        client_iqn, gw))
                    continue
                else:
                    raise GatewayAPIError(response.text)
        else:
            raise GatewayAPIError(response.text)

        self.logger.info('ok')
Ejemplo n.º 10
0
    def ui_command_disk(self, action='add', disk=None):
        """
        Disks can be added or removed from the client one at a time using
        the disk sub-command. Note that the disk MUST already be defined
        within the configuration

        > disk add|remove <disk_name>

        Adding a disk will result in the disk occupying the client's next
        available lun id.
        Removing a disk will preserve existing lun id allocations

        """

        valid_actions = ['add', 'remove']

        current_luns = self._get_lun_names()

        if action == 'add':

            valid_disk_names = [
                defined_disk.image_id for defined_disk in
                self.parent.parent.parent.parent.disks.children
            ]
        else:
            valid_disk_names = current_luns

        if not disk:
            self.logger.critical(
                "You must supply a disk name to add/remove from this client")
            return

        if action not in valid_actions:
            self.logger.error(
                "you can only add and remove disks - {} is invalid ".format(
                    action))
            return

        if disk not in valid_disk_names:
            self.logger.critical(
                "the request to {} disk '{}' is invalid".format(action, disk))
            return

        # At this point we are either in add/remove mode, with a valid disk to act upon
        self.logger.debug("Client '{}' update - {} disk {}".format(
            self.client_iqn, action, disk))

        if action == 'add':
            current_luns.append(disk)
        else:
            current_luns.remove(disk)

        image_list = ','.join(current_luns)

        other_gateways = get_other_gateways(
            self.parent.parent.parent.parent.target.children)

        api_vars = {
            "committing_host": this_host(),
            "image_list": image_list,
            "chap": self.auth['chap']
        }

        clientlun_api = '{}://127.0.0.1:{}/api/clientlun/{}'.format(
            self.http_mode, settings.config.api_port, self.client_iqn)

        response = put(clientlun_api,
                       data=api_vars,
                       auth=(settings.config.api_user,
                             settings.config.api_password),
                       verify=settings.config.api_ssl_verify)

        if response.status_code == 200:

            if action == 'add':

                # The addition of the lun will get a lun id assigned so
                # we need to query the api server to get the new configuration
                # to be able to set the local cli entry correctly
                get_api_vars = {"disk": disk}
                response = get(clientlun_api,
                               data=get_api_vars,
                               auth=(settings.config.api_user,
                                     settings.config.api_password),
                               verify=settings.config.api_ssl_verify)

                if response.status_code == 200:
                    lun_dict = json.loads(response.text)['message']
                    lun_id = lun_dict[disk]['lun_id']
                    MappedLun(self, disk, lun_id)
                else:
                    raise GatewayAPIError(response.text)

            else:

                # this was a remove request, so simply delete the child
                # MappedLun object corresponding to this rbd name

                mlun = [lun for lun in self.children
                        if lun.rbd_name == disk][0]
                self.remove_child(mlun)

            self.logger.debug("- local environment updated")

            for gw in other_gateways:
                clientlun_api = '{}://{}:{}/api/clientlun/{}'.format(
                    self.http_mode, gw, settings.config.api_port,
                    self.client_iqn)

                response = put(clientlun_api,
                               data=api_vars,
                               auth=(settings.config.api_user,
                                     settings.config.api_password),
                               verify=settings.config.api_ssl_verify)

                if response.status_code == 200:
                    self.logger.debug("- gateway '{}' updated".format(gw))
                    continue
                else:
                    raise GatewayAPIError(response.text)
        else:
            raise GatewayAPIError(response.text)

        self.logger.info('ok')
Ejemplo n.º 11
0
    def ui_command_auth(self, chap=None):
        """
        Client authentication can be set to use CHAP by supplying the
        a string of the form <username>/<password>

        > auth chap=myserver/mypassword2016

        username ... The username is freeform, but would normally be the
                     hostname or iqn
        password ... the password must be between 12-16 chars in length
                     containing alphanumeric characters plus the following
                     special characters !,&,_

        """

        if not chap:
            self.logger.error(
                "To set CHAP authentication provide a string of the format 'user/password'"
            )
            return

        else:
            # validate the chap credentials are acceptable
            if not Client.valid_credentials(chap, auth_type='chap'):
                self.logger.error(
                    "-> the format of the CHAP string is invalid, use 'help auth' for examples"
                )
                return

        self.logger.debug("Client '{}' AUTH update : {}".format(
            self.client_iqn, chap))
        # get list of children (luns) to build current image list
        image_list = ','.join(self._get_lun_names())

        other_gateways = get_other_gateways(
            self.parent.parent.parent.parent.target.children)
        api_vars = {
            "committing_host": this_host(),
            "image_list": image_list,
            "chap": chap
        }

        clientauth_api = '{}://127.0.0.1:{}/api/clientauth/{}'.format(
            self.http_mode, settings.config.api_port, self.client_iqn)

        response = put(clientauth_api,
                       data=api_vars,
                       auth=(settings.config.api_user,
                             settings.config.api_password),
                       verify=settings.config.api_ssl_verify)

        if response.status_code == 200:
            self.logger.debug("- Local environment updated")

            self.auth['chap'] = chap

            for gw in other_gateways:
                clientauth_api = '{}://{}:{}/api/clientauth/{}'.format(
                    self.http_mode, gw, settings.config.api_port,
                    self.client_iqn)

                response = put(clientauth_api,
                               data=api_vars,
                               auth=(settings.config.api_user,
                                     settings.config.api_password),
                               verify=settings.config.api_ssl_verify)

                if response.status_code == 200:
                    self.logger.debug("- {} updated".format(gw))
                    continue
                else:
                    raise GatewayAPIError(response.text)
        else:
            raise GatewayAPIError(response.text)

        self.logger.info('ok')
Ejemplo n.º 12
0
    def ui_command_delete(self, client_iqn):
        """
        You may delete a client from the configuration, but you must ensure that
        the client has logged out of the iscsi gateways. Attempting to delete a
        client that has an open session will fail the request

        > delete <client_iqn>

        """
        # check the iqn given matches one of the child objects - i.e. it's valid
        client_names = [child.name for child in self.children]
        if client_iqn not in client_names:
            self.logger.error(
                "Host with an iqn of '{}' is not defined...mis-typed?".format(
                    client_iqn))
            return

        lio_root = root.RTSRoot()
        clients_logged_in = [
            session['parent_nodeacl'].node_wwn for session in lio_root.sessions
            if session['state'] == 'LOGGED_IN'
        ]

        if client_iqn in clients_logged_in:
            self.logger.error(
                "Host '{}' is logged in - unable to delete until it's logged out"
                .format(client_iqn))
            return

        # At this point we know the client requested is defined to the configuration
        # and is not currently logged in (at least to this host), OK to delete
        self.logger.debug("Client DELETE for {}".format(client_iqn))
        client = [
            client for client in self.children if client.name == client_iqn
        ][0]

        # Process flow: remote gateways > local > delete config object entry

        other_gateways = get_other_gateways(
            self.parent.parent.parent.target.children)
        api_vars = {"committing_host": this_host()}

        for gw in other_gateways:
            client_api = '{}://{}:{}/api/client/{}'.format(
                self.http_mode, gw, settings.config.api_port, client_iqn)

            response = delete(client_api,
                              data=api_vars,
                              auth=(settings.config.api_user,
                                    settings.config.api_password),
                              verify=settings.config.api_ssl_verify)

            if response.status_code == 200:
                self.logger.debug("- '{}' removed from {}".format(
                    client_iqn, gw))
                continue
            elif response.status_code == 400:
                self.logger.critical("- '{}' is in use on {}".format(
                    client_iqn, gw))
                return
            else:
                raise GatewayAPIError(response.text)

        # At this point the other gateways have removed the client, so
        # remove from the local instance and delete from the interface
        client_api = '{}://127.0.0.1:{}/api/client/{}'.format(
            self.http_mode, settings.config.api_port, client_iqn)

        response = delete(client_api,
                          data=api_vars,
                          auth=(settings.config.api_user,
                                settings.config.api_password),
                          verify=settings.config.api_ssl_verify)

        if response.status_code == 200:

            self.logger.debug(
                "- '{}' removed from local gateway, configuration updated".
                format(client_iqn))
            self.delete(client)

        self.logger.info('ok')