Exemple #1
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. Once a client is created the admin is automatically placed in the
        context of the new client definition for auth and disk configuration
        operations.

        e.g.
        > create <client_iqn>

        """
        self.logger.debug("CMD: ../hosts/ create {}".format(client_iqn))
        cli_seed = {"luns": {}, "auth": {}}

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

        target_iqn = self.parent.name

        # Issue the API call to create the client
        client_api = ('{}://localhost:{}/api/'
                      'client/{}/{}'.format(self.http_mode,
                                            settings.config.api_port,
                                            target_iqn,
                                            client_iqn))

        self.logger.debug("Client CREATE for {}".format(client_iqn))
        api = APIRequest(client_api)
        api.put()

        if api.response.status_code == 200:
            Client(self, client_iqn, cli_seed)
            self.config = get_config()
            self.logger.debug("- Client '{}' added".format(client_iqn))
            self.logger.info('ok')

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

        # switch the current directory to the new client for auth or disk
        # definitions as part of the users workflow
        return self.ui_command_cd(client_iqn)
Exemple #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 = ('{}://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(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)))
Exemple #3
0
    def ui_command_auth(self, action=None):
        """
        Disable/enable ACL authentication or clear CHAP settings for all clients on the target.

        - disable_acl ... Disable initiator name based ACL authentication.

        - enable_acl .... Enable initiator name based ACL authentication.

        - nochap ........ Remove chap authentication for all clients across all gateways.
                          Initiator name based authentication will then be used.

        e.g.
        auth disable_acl

        """

        if not action:
            self.logger.error(
                "Missing auth argument. Use 'auth nochap|disable_acl|enable_acl'"
            )
            return

        if action not in ['nochap', 'enable_acl', 'disable_acl']:
            self.logger.error(
                "Invalid auth argument. Use 'auth nochap|disable_acl|enable_acl'"
            )
            return

        if action == 'nochap':
            for client in self.children:
                client.set_auth(action, None, None, None)
        else:
            target_iqn = self.parent.name
            api_vars = {'action': action}
            targetauth_api = ('{}://localhost:{}/api/'
                              'targetauth/{}'.format(self.http_mode,
                                                     settings.config.api_port,
                                                     target_iqn))
            api = APIRequest(targetauth_api, data=api_vars)
            api.put()
            if api.response.status_code == 200:
                self.config = get_config()
                self.logger.info('ok')
            else:
                self.logger.error("Failed to {}: "
                                  "{}".format(
                                      action,
                                      response_message(api.response,
                                                       self.logger)))
                return
Exemple #4
0
    def snapshot(self, action, name):
        self.logger.debug("CMD: /disks/{} snapshot action={} "
                          "name={}".format(self.image_id, action, name))

        valid_actions = ['create', 'delete', 'rollback']
        if action not in valid_actions:
            self.logger.error("you can only create, delete, or rollback - "
                              "{} is invalid ".format(action))
            return

        if action == 'create':
            if name in self.snapshot_names:
                self.logger.error("Snapshot {} already exists".format(name))
                return
            if not valid_snapshot_name(name):
                self.logger.error(
                    "Snapshot {} contains invalid characters".format(name))
                return
        else:
            if name not in self.snapshot_names:
                self.logger.error("Snapshot {} does not exist".format(name))
                return

        if action == 'rollback':
            self.logger.warning("Please be patient, rollback might take time")

        self.logger.debug("Issuing snapshot {} request".format(action))
        disk_api = ('{}://localhost:{}/api/'
                    'disksnap/{}/{}'.format(self.http_mode,
                                            settings.config.api_port,
                                            self.image_id, name))

        if action == 'delete':
            api = APIRequest(disk_api)
            api.delete()
        else:
            api_vars = {'mode': action}

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

        if api.response.status_code == 200:
            if action == 'create' or action == 'delete':
                self._refresh_config()
            self.logger.info('ok')
        else:
            self.logger.error("Failed to {} snapshot: "
                              "{}".format(
                                  action,
                                  response_message(api.response, self.logger)))
Exemple #5
0
    def resize(self, size):
        """
        Perform the resize operation, and sync the disk size across each of the
        gateways
        :param size: (int) new size for the rbd image
        :return:
        """
        # resize is actually managed by the same lun and api endpoint as
        # create so this logic is very similar to a 'create' request

        size_rqst = size.upper()
        if not valid_size(size_rqst):
            self.logger.error("Size is invalid")
            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()

        # Issue the api request for the resize
        disk_api = ('{}://localhost:{}/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("Issuing resize request")
        api = APIRequest(disk_api, data=api_vars)
        api.put()

        if api.response.status_code == 200:
            # at this point the resize request was successful, so we need to
            # update the ceph pool meta data (%commit etc)
            self._update_pool()
            self.size_h = size_rqst
            self.size = convert_2_bytes(size_rqst)

            self.logger.info('ok')

        else:
            self.logger.error("Failed to resize : "
                              "{}".format(
                                  response_message(api.response, self.logger)))
Exemple #6
0
    def set_auth(self, chap=None):

        self.logger.debug("CMD: ../hosts/<client_iqn> auth *")

        if not chap:
            self.logger.error("To set or reset authentication, specify either "
                              "chap=<user>/<password> or nochap")
            return

        if chap == 'nochap':
            chap = ''
        else:
            # string could have been supplied as chap=user/password or
            # simply user/password - either way all we see is user/password
            if '/' not in chap:
                self.logger.error(
                    "CHAP format is invalid - must be a <username>/<password> "
                    "format. Use 'help auth' to show the correct syntax and "
                    "supported characters")
                return

        self.logger.debug("CHAP to be set to '{}' for '{}'".format(
            chap, self.client_iqn))

        api_vars = {"chap": chap}

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

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

        if api.response.status_code == 200:
            self.logger.debug("- client credentials updated")
            if chap != '':
                self.auth['chap'] = chap
            else:
                self.auth['chap'] = "None"

            self.logger.info('ok')

        else:
            self.logger.error("Failed to update the client's auth"
                              " :{}".format(
                                  response_message(api.response, self.logger)))
            return
Exemple #7
0
    def ui_command_auth(self, nochap=False, chap=None):
        """
        Client authentication can be set to use CHAP by supplying the
        a string of the form <username>/<password>

        e.g.
        auth nochap | chap=username/password

        username ... The username is freeform, but would typically be the
                     host's shortname or iqn
        password ... the password must be between 12-16 chars in length
                     containing alphanumeric characters, plus the following
                     special characters @,_,-

        Specifying 'nochap' will remove chap authentication for the client
        across all gateways.

        """

        self.logger.debug("CMD: ../hosts/<client_iqn> auth *")

        if nochap:
            chap = ''

        self.logger.debug("Client '{}' AUTH update".format(self.client_iqn))

        api_vars = {"chap": chap}

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

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

        if api.response.status_code == 200:
            self.logger.debug("- client credentials updated")
            self.auth['chap'] = chap
            self.logger.info('ok')

        else:
            self.logger.error("Failed to update the client's auth"
                              " :{}".format(api.response.json()['message']))
            return
Exemple #8
0
    def ui_command_create(self, group_name):
        """
        Create a host group definition. Group names can be use up to 32
        alphanumeric characters, including '_', '-' and '@'. Note that once a
        group is created it can not be renamed.
        """
        self.logger.debug("CMD: ../host-groups/ create {}".format(group_name))

        if group_name in self.groups:
            self.logger.error("Group {} already defined".format(group_name))
            return

        grp_regex = re.compile(r"^[\w\@\-\_]{{1,{}}}$".format(
            HostGroups.group_name_length))
        if not grp_regex.search(group_name):
            self.logger.error("Invalid group name - max of {} chars of "
                              "alphanumeric and -,_,@ "
                              "characters".format(
                                  HostGroups.group_name_length))
            return

        target_iqn = self.parent.name

        # this is a new group
        group_api = ('{}://{}:{}/api/hostgroup/'
                     '{}/{}'.format(self.http_mode, "localhost",
                                    settings.config.api_port, target_iqn,
                                    group_name))

        api = APIRequest(group_api)
        api.put()
        if api.response.status_code != 200:
            self.logger.error("Failed : "
                              "{}".format(
                                  response_message(api.response, self.logger)))
            return

        self.logger.debug('Adding group to the UI')
        HostGroup(self, group_name)

        self.logger.info('ok')

        # Switch to the new group
        return self.ui_command_cd(group_name)
Exemple #9
0
    def ui_command_discovery_auth(self, chap=None, chap_mutual=None):
        """
        Discovery authentication can be set to use CHAP/CHAP_MUTUAL by supplying
        strings of the form <username>/<password>

        Specifying 'nochap' will remove discovery authentication.

        e.g.
        auth chap=username/password chap_mutual=username/password

        """

        self.logger.warn("discovery chap={}, chap_mutual={}".format(chap, chap_mutual))

        self.logger.debug("CMD: /iscsi discovery_auth")

        if not chap:
            self.logger.error("To set or reset discovery authentication, specify either "
                              "chap=<user>/<password> [chap_mutual]=<user>/<password> or nochap")
            return

        if chap == 'nochap':
            chap = ''
        if not chap_mutual:
            chap_mutual = ''

        self.logger.debug("discovery auth to be set to chap='{}', chap_mutual='{}'".format(
            chap, chap_mutual))

        api_vars = {"chap": chap, "chap_mutual": chap_mutual}
        discoveryauth_api = ('{}://localhost:{}/api/'
                             'discoveryauth'.format(self.http_mode,
                                                    settings.config.api_port))
        api = APIRequest(discoveryauth_api, data=api_vars)
        api.put()

        if api.response.status_code == 200:
            self._set_auth(chap, chap_mutual)
            self.logger.info('ok')
        else:
            self.logger.error("Error: {}".format(response_message(api.response, self.logger)))
            return
Exemple #10
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)))
Exemple #11
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. Once a client is created the admin is automatically placed in the
        context of the new client definition for auth and disk configuration
        operations.

        e.g.
        > create <client_iqn>

        """
        self.logger.debug("CMD: ../hosts/ create {}".format(client_iqn))
        cli_seed = {"luns": {}, "auth": {}}

        # Issue the API call to create the client
        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))
        api = APIRequest(client_api)
        api.put()

        if api.response.status_code == 200:
            Client(self, client_iqn, cli_seed)
            self.logger.debug("- Client '{}' added".format(client_iqn))
            self.logger.info('ok')

        else:
            self.logger.error("Failed: {}".format(
                api.response.json()['message']))
            return

        # switch the current directory to the new client for auth or disk
        # definitions as part of the users workflow
        return self.ui_command_cd(client_iqn)
Exemple #12
0
    def reconfigure(self, attribute, value):
        controls = {attribute: value}
        controls_json = json.dumps(controls)

        ui_root = self.get_ui_root()
        disk = ui_root.disks.disk_lookup[self.image_id]
        if not disk.owner:
            self.logger.error(
                "Cannot reconfigure until disk assigned to target")
            return

        local_gw = this_host()

        # Issue the api request for reconfigure
        disk_api = ('{}://localhost:{}/api/'
                    'disk/{}'.format(self.http_mode, settings.config.api_port,
                                     self.image_id))

        api_vars = {
            'pool': self.pool,
            'owner': local_gw,
            'controls': controls_json,
            'mode': 'reconfigure'
        }

        self.logger.debug("Issuing reconfigure request: attribute={}, "
                          "value={}".format(attribute, value))
        api = APIRequest(disk_api, data=api_vars)
        api.put()

        if api.response.status_code == 200:
            self.logger.info('ok')
            self._refresh_config()
        else:
            self.logger.error("Failed to reconfigure : "
                              "{}".format(
                                  response_message(api.response, self.logger)))
Exemple #13
0
    def ui_command_disk(self, action='add', disk=None, size=None):
        """
        Disks can be added or removed from the client one at a time using
        the 'disk' sub-command. Note that if the disk does not currently exist
        in the configuration, the cli will attempt to create it for you.

        e.g.
        disk add <pool_name.image_name> <size>
        disk remove <pool_name.image_name>

        Adding a disk will result in the disk occupying the client's next
        available lun id. Once allocated removing a LUN will not change the
        LUN id associations for the client.

        Note that if the client is a member of a host group, disk management
        *must* be performed at the group level. Attempting to add/remove disks
        at the client level will fail.

        """

        self.logger.debug("CMD: ../hosts/<client_iqn> disk action={}"
                          " disk={}".format(action, disk))

        valid_actions = ['add', 'remove']

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

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

        lun_list = [(lun.rbd_name, lun.lun_id) for lun in self.children]
        current_luns = Client.get_srtd_names(lun_list)

        if action == 'add':

            if disk not in current_luns:
                ui_root = self.get_ui_root()
                valid_disk_names = [
                    defined_disk.image_id
                    for defined_disk in ui_root.disks.children
                ]
            else:
                # disk provided is already mapped, so remind the user
                self.logger.error("Disk {} already mapped".format(disk))
                return
        else:
            valid_disk_names = current_luns

        if disk not in valid_disk_names:

            # if this is an add operation, we can create the disk on-the-fly
            # for the admin

            if action == 'add':
                ui_root = self.get_ui_root()
                ui_disks = ui_root.disks
                if not size:
                    self.logger.error("To auto-define the disk to the client"
                                      " you must provide a disk size")
                    return

                # a disk given here would be of the form pool.image
                try:
                    pool, image = disk.split('.')
                except ValueError:
                    self.logger.error(
                        "Invalid format. Use pool_name.disk_name")
                    return

                rc = ui_disks.create_disk(pool=pool, image=image, size=size)
                if rc == 0:
                    self.logger.debug("disk auto-define successful")
                else:
                    self.logger.error("disk auto-define failed({}), try "
                                      "using the /disks create "
                                      "command".format(rc))
                    return

            else:
                self.logger.error("disk '{}' is not mapped to this "
                                  "client ".format(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))

        api_vars = {"disk": disk}

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

        api = APIRequest(clientlun_api, data=api_vars)
        if action == 'add':
            api.put()
        else:
            api.delete()

        if api.response.status_code == 200:

            self.logger.debug("disk mapping updated successfully")

            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}

                clientlun_api = clientlun_api.replace('/clientlun/',
                                                      '/_clientlun/')

                self.logger.debug("Querying API to get mapped LUN information")
                api = APIRequest(clientlun_api, data=get_api_vars)
                api.get()

                if api.response.status_code == 200:
                    try:
                        lun_dict = api.response.json()['message']
                    except Exception:
                        self.logger.error("Malformed REST API response")
                        return

                    # now update the UI
                    lun_id = lun_dict[disk]['lun_id']
                    self.add_lun(disk, lun_id)

                else:
                    self.logger.error("Query for disk '{}' meta data "
                                      "failed".format(disk))
                    return

            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_lun(mlun)

            self.logger.debug("configuration update successful")
            self.logger.info('ok')

        else:
            # the request to add/remove the disk for the client failed
            self.logger.error("disk {} for '{}' against {} failed"
                              "\n{}".format(
                                  action, disk, self.client_iqn,
                                  response_message(api.response, self.logger)))
            return
Exemple #14
0
def all_disk(image_id):
    """
    Coordinate the create/delete of rbd images across the gateway nodes
    The "all_" method calls the corresponding disk api entrypoints across each
    gateway. Processing is done serially: creation is done locally first,
    then other gateways - whereas, rbd deletion is performed first against
    remote gateways and then the local machine is used to perform the actual
    rbd delete.

    :param image_id: (str) rbd image name of the format pool.image
    **RESTRICTED**
    """

    http_mode = 'https' if settings.config.api_secure else 'http'

    local_gw = this_host()
    logger.debug("this host is {}".format(local_gw))
    gateways = [
        key for key in config.config['gateways']
        if isinstance(config.config['gateways'][key], dict)
    ]
    logger.debug("other gateways - {}".format(gateways))
    gateways.remove(local_gw)
    logger.debug("other gw's {}".format(gateways))

    if request.method == 'PUT':

        pool = request.form.get('pool')
        size = request.form.get('size')
        mode = request.form.get('mode')

        pool, image_name = image_id.split('.')

        disk_usable = valid_disk(pool=pool,
                                 image=image_name,
                                 size=size,
                                 mode=mode)
        if disk_usable != 'ok':
            return jsonify(message=disk_usable), 400

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

        api_vars = {
            'pool': pool,
            'size': size,
            'owner': local_gw,
            'mode': mode
        }

        logger.debug("Issuing disk request to the local API "
                     "for {}".format(image_id))

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

        if api.response.status_code == 200:
            logger.info("LUN is ready on this host")

            for gw in gateways:
                logger.debug("Adding {} to gw {}".format(image_id, gw))
                disk_api = '{}://{}:{}/api/disk/{}'.format(
                    http_mode, gw, settings.config.api_port, image_id)
                api = APIRequest(disk_api, data=api_vars)
                api.put()

                if api.response.status_code == 200:
                    logger.info("LUN is ready on {}".format(gw))
                else:
                    return jsonify(message=api.response.json()['message']), 500

        else:
            logger.error(api.response.json()['message'])
            return jsonify(message=api.response.json()['message']), 500

        logger.info("LUN defined to all gateways for {}".format(image_id))

        return jsonify(message="ok"), 200

    else:
        # this is a DELETE request
        pool_name, image_name = image_id.split('.')
        disk_usable = valid_disk(mode='delete',
                                 pool=pool_name,
                                 image=image_name)

        if disk_usable != 'ok':
            return jsonify(message=disk_usable), 400

        api_vars = {'purge_host': local_gw}

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

            logger.debug("removing '{}' from {}".format(image_id, gw_name))

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

            if api.response.status_code == 200:
                logger.debug("{} removed from {}".format(image_id, gw_name))

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

        # 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(
            http_mode, settings.config.api_port, image_id)

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

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

        if api.response.status_code == 200:
            logger.debug("- rbd {} deleted".format(image_id))
            return jsonify(message="ok"), 200
        else:
            return jsonify(message="failed to delete rbd "
                           "{}".format(image_id)), 500
Exemple #15
0
def all_client_auth(client_iqn):
    """
    Coordinate client authentication changes across each gateway node
    The following parameters are needed to manage client auth
    :param client_iqn: (str) client IQN name
    :param chap: (str) chap string of the form user/password or ''
    **RESTRICTED**
    """

    http_mode = 'https' if settings.config.api_secure else 'http'
    local_gw = this_host()
    logger.debug("this host is {}".format(local_gw))
    gateways = [
        key for key in config.config['gateways']
        if isinstance(config.config['gateways'][key], dict)
    ]
    logger.debug("other gateways - {}".format(gateways))
    gateways.remove(local_gw)

    lun_list = config.config['clients'][client_iqn]['luns'].keys()
    image_list = ','.join(lun_list)
    chap = request.form.get('chap')

    client_usable = valid_client(mode='auth', client_iqn=client_iqn, chap=chap)
    if client_usable != 'ok':
        logger.error("BAD auth request from {}".format(request.remote_addr))
        return jsonify(message=client_usable), 400

    api_vars = {
        "committing_host": local_gw,
        "image_list": image_list,
        "chap": chap
    }

    clientauth_api = '{}://127.0.0.1:{}/api/clientauth/{}'.format(
        http_mode, settings.config.api_port, client_iqn)
    logger.debug("Issuing client update to local gw for {}".format(client_iqn))

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

    if api.response.status_code == 200:
        logger.debug("Client update succeeded on local LIO")

        for gw in gateways:
            clientauth_api = '{}://{}:{}/api/clientauth/{}'.format(
                http_mode, gw, settings.config.api_port, client_iqn)
            logger.debug("updating client {} on {}".format(client_iqn, gw))
            api = APIRequest(clientauth_api, data=api_vars)
            api.put()
            if api.response.status_code == 200:
                logger.info("client update successful on {}".format(gw))
                continue
            else:
                return jsonify(message="client update failed on "
                                       "{}".format(gw)), \
                       api.response.status_code

        logger.info("All gateways updated")
        return jsonify(message="ok"), 200

    else:
        # the local update failed, so abort further updates
        return jsonify(message="Client updated failed on local "
                       "LIO instance"), api.response.status_code
Exemple #16
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
Exemple #17
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
Exemple #18
0
def all_client_luns(client_iqn):
    """
    Coordinate the addition(PUT) and removal(DELETE) of a disk from a client
    :param client_iqn: (str) IQN of the client
    :param disk: (str) rbd image name of the format pool.image
    **RESTRICTED**
    """

    http_mode = 'https' if settings.config.api_secure else 'http'

    local_gw = this_host()
    logger.debug("this host is {}".format(local_gw))
    gateways = [
        key for key in config.config['gateways']
        if isinstance(config.config['gateways'][key], dict)
    ]
    logger.debug("other gateways - {}".format(gateways))
    gateways.remove(local_gw)

    disk = request.form.get('disk')

    lun_list = config.config['clients'][client_iqn]['luns'].keys()

    if request.method == 'PUT':
        lun_list.append(disk)
    else:
        # this is a delete request
        if disk in lun_list:
            lun_list.remove(disk)
        else:
            return jsonify(message="disk not mapped to client"), 400

    chap_obj = CHAP(config.config['clients'][client_iqn]['auth']['chap'])
    chap = "{}/{}".format(chap_obj.user, chap_obj.password)
    image_list = ','.join(lun_list)

    client_usable = valid_client(mode='disk',
                                 client_iqn=client_iqn,
                                 image_list=image_list)
    if client_usable != 'ok':
        logger.error("Bad disk request for client {} : "
                     "{}".format(client_iqn, client_usable))
        return jsonify(message=client_usable), 400

    # committing host is the local LIO node
    api_vars = {
        "committing_host": local_gw,
        "image_list": image_list,
        "chap": chap
    }

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

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

    if api.response.status_code == 200:

        logger.info("disk mapping update for {} successful".format(client_iqn))

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

            logger.debug("Updating disk map for {} on GW {}".format(
                client_iqn, gw))
            api = APIRequest(clientlun_api, data=api_vars)
            api.put()

            if api.response.status_code == 200:
                logger.debug("gateway '{}' updated".format(gw))
                continue
            else:
                logger.error("disk mapping update on {} failed".format(gw))
                return jsonify(message="disk map updated failed on "
                                       "{}".format(gw)), \
                       api.response.status_code

        return jsonify(message="ok"), 200

    else:
        # disk map update failed at the first hurdle!
        logger.error("disk map update failed on the local LIO instance")
        return jsonify(message="failed to update local LIO instance"), \
               api.response.status_code
Exemple #19
0
def all_client(client_iqn):
    """
    Handle the client create/delete actions across gateways
    :param client_iqn: (str) IQN of the client to create or delete
    **RESTRICTED**
    Examples:
    curl --insecure --user admin:admin -X PUT https://192.168.122.69:5001/api/all_client/iqn.1994-05.com.redhat:myhost4
    curl --insecure --user admin:admin -X DELETE https://192.168.122.69:5001/api/all_client/iqn.1994-05.com.redhat:myhost4
    """

    method = {"PUT": 'create', "DELETE": 'delete'}

    http_mode = 'https' if settings.config.api_secure else 'http'
    local_gw = this_host()
    logger.debug("this host is {}".format(local_gw))
    gateways = [
        key for key in config.config['gateways']
        if isinstance(config.config['gateways'][key], dict)
    ]
    logger.debug("other gateways - {}".format(gateways))
    gateways.remove(local_gw)

    # committing host is the node responsible for updating the config object
    api_vars = {"committing_host": local_gw}

    # validate the PUT/DELETE request first
    client_usable = valid_client(mode=method[request.method],
                                 client_iqn=client_iqn)
    if client_usable != 'ok':
        return jsonify(message=client_usable), 400

    if request.method == 'PUT':

        client_api = '{}://127.0.0.1:{}/api/client/{}'.format(
            http_mode, settings.config.api_port, client_iqn)

        logger.debug("Processing client CREATE for {}".format(client_iqn))
        api = APIRequest(client_api, data=api_vars)
        api.put()
        if api.response.status_code == 200:
            logger.info("Client {} added to local LIO".format(client_iqn))

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

                logger.debug("sending request to {} to create {}".format(
                    gw, client_iqn))
                api = APIRequest(client_api, data=api_vars)
                api.put()

                if api.response.status_code == 200:
                    logger.info("Client '{}' added to {}".format(
                        client_iqn, gw))
                    continue
                else:
                    # client create failed against the remote LIO instance
                    msg = api.response.json()['message']
                    logger.error("Client create for {} failed on {} "
                                 ": {}".format(client_iqn, gw, msg))

                    return jsonify(message=msg), 500

            # all gateways processed return a success state to the caller
            return jsonify(message='ok'), 200

        else:
            # client create failed against the local LIO instance
            msg = api.response.json()['message']
            logger.error("Client create on local LIO instance failed "
                         "for {} : {}".format(client_iqn, msg))
            return jsonify(message=msg), 500

    else:
        # DELETE client request
        # Process flow: remote gateways > local > delete config object entry
        for gw in gateways:
            client_api = '{}://{}:{}/api/client/{}'.format(
                http_mode, gw, settings.config.api_port, client_iqn)
            logger.info("- removing '{}' from {}".format(client_iqn, gw))
            api = APIRequest(client_api, data=api_vars)
            api.delete()

            if api.response.status_code == 200:
                logger.info("- '{}' removed".format(client_iqn))
                continue
            elif api.response.status_code == 400:
                logger.error("- '{}' is in use on {}".format(client_iqn, gw))
                return jsonify(message="Client in use"), 400
            else:
                msg = api.response.json()['message']
                logger.error("Failed to remove {} from {}".format(
                    client_iqn, gw))
                return jsonify(message="failed to remove client '{}' on "
                               "{}".format(client_iqn, msg)), 500

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

        if api.response.status_code == 200:
            logger.info("successfully removed '{}'".format(client_iqn))
            return jsonify(message="ok"), 200

        else:
            return jsonify(message="Unable to delete {} from local LIO "
                                   "instance".format(client_iqn)), \
                   api.response.status_code
Exemple #20
0
def all_gateway(gateway_name=None):
    """
    Define iscsi gateway(s) across node(s), adding TPGs, disks and clients
    The call requires the following variables to be set;
    :param gateway_name: (str) gateway name
    :param ip_address: (str) ipv4 dotted quad for the address iSCSI should use
    :param nosync: (bool) whether to sync the LIO objects to the new gateway
    **RESTRICTED**
    """

    ip_address = request.form.get('ip_address')
    nosync = request.form.get('nosync', False)

    # first confirm that the request is actually valid, if not return a 400
    # error with the error description
    current_config = config.config
    gateway_usable = valid_gateway(gateway_name, ip_address, current_config)
    if gateway_usable != 'ok':
        return jsonify(message=gateway_usable), 400

    resp_text = "Gateway added"  # Assume the best!
    http_mode = 'https' if settings.config.api_secure else 'http'

    current_disks = config.config['disks']
    current_clients = config.config['clients']
    target_iqn = config.config['gateways'].get('iqn')

    total_objects = (len(current_disks.keys()) + len(current_clients.keys()))
    if total_objects == 0:
        nosync = True

    gateway_ip_list = config.config['gateways'].get('ip_list', [])
    gateway_ip_list.append(ip_address)

    first_gateway = (len(gateway_ip_list) == 1)

    for endpoint in gateway_ip_list:
        if first_gateway:
            endpoint = '127.0.0.1'

        logger.debug("Processing GW endpoint {} for {}".format(
            endpoint, gateway_name))

        api_endpoint = '{}://{}:{}/api'.format(http_mode, endpoint,
                                               settings.config.api_port)

        gw_rqst = api_endpoint + '/gateway/{}'.format(gateway_name)
        gw_vars = {
            "target_iqn": target_iqn,
            "gateway_ip_list": ",".join(gateway_ip_list),
            "mode": "target"
        }

        logger.debug("Calling API at {} with {}".format(gw_rqst, gw_vars))

        api = APIRequest(gw_rqst, data=gw_vars)
        api.put()
        if api.response.status_code != 200:
            # GW creation failed
            msg = api.response.json()['message']

            logger.error("Failed to create gateway {}: {}".format(
                gateway_name, msg))

            return jsonify(message="Failed to create gateway"), 500

        # for the new gateway, when sync is selected we need to run the
        # disk api to register all the rbd's to that gateway
        if endpoint == ip_address and not nosync:

            for disk_key in current_disks:

                this_disk = current_disks[disk_key]
                lun_rqst = api_endpoint + '/disk/{}'.format(disk_key)
                lun_vars = {
                    "pool": this_disk['pool'],
                    "size": "0G",
                    "owner": this_disk['owner'],
                    "mode": "sync"
                }

                api = APIRequest(lun_rqst, data=lun_vars)
                api.put()
                if api.response.status_code != 200:
                    msg = api.response.json()['message']
                    logger.error("Failed to add disk {} to {} new "
                                 "tpg : {}".format(disk_key, endpoint, msg))
                    return jsonify(message="Failed to add disk"), 500

            resp_text += ", {} disks added".format(len(current_disks))

        # Adding a gateway introduces a new tpg - each tpg MUST have the
        # luns defined so a RTPG call can be responded to correctly, so
        # we need to sync the disks to the new tpg's
        if len(current_disks.keys()) > 0:

            if endpoint != ip_address or not nosync:

                gw_vars['mode'] = 'map'
                api = APIRequest(gw_rqst, data=gw_vars)
                api.put()
                if api.response.status_code != 200:
                    # GW creation failed - if the failure was severe you'll
                    # see a json issue here.
                    msg = api.response.json()['message']
                    logger.error("Failed to map existing disks to new"
                                 " tpg on {} - ".format(endpoint))
                    return jsonify(message="Failed to map disk"), 500

            if endpoint == ip_address and not nosync:

                for client_iqn in current_clients:

                    this_client = current_clients[client_iqn]
                    client_luns = this_client['luns']
                    lun_list = [(disk, client_luns[disk]['lun_id'])
                                for disk in client_luns]
                    srtd_list = Client.get_srtd_names(lun_list)

                    # client_iqn, image_list, chap, committing_host
                    client_vars = {
                        'chap': this_client['auth']['chap'],
                        'image_list': ','.join(srtd_list),
                        'committing_host': local_gw
                    }

                    api = APIRequest(api_endpoint +
                                     "/client/{}".format(client_iqn),
                                     data=client_vars)
                    api.put()
                    if api.response.status_code != 200:
                        msg = api.response.json()['message']
                        logger.error("Problem adding client {} - "
                                     "{}".format(
                                         client_iqn,
                                         api.response.json()['message']))
                        return jsonify(message="Failed to add client"), 500

                resp_text += ", {} clients defined".format(
                    len(current_clients))

    return jsonify(message=resp_text), 200
Exemple #21
0
    def ui_command_auth(self,
                        username=None,
                        password=None,
                        mutual_username=None,
                        mutual_password=None):
        """
        Target authentication can be set to use CHAP/CHAP_MUTUAL by supplying
        username, password, mutual_username, mutual_password

        e.g.
        auth username=<user> password=<pass> mutual_username=<m_user> mutual_password=<m_pass>

        username / mutual_username ... the username is 8-64 character string. Each character
                                       may either be an alphanumeric or use one of the following
                                       special characters .,:,-,@.
                                       Consider using the hosts 'shortname' or the initiators IQN
                                       value as the username

        password / mutual_password ... the password must be between 12-16 chars in length
                                       containing alphanumeric characters, plus the following
                                       special characters @,_,-,/
        """

        self.logger.debug("CMD: /iscsi-targets/<target_iqn> auth *")

        if not username:
            self.logger.error(
                "To set authentication, specify "
                "username=<user> password=<password> "
                "[mutual_username]=<user> [mutual_password]=<password> "
                "or nochap")
            return

        if username == 'nochap':
            username = ''
            password = ''
            mutual_username = ''
            mutual_password = ''

        self.logger.debug(
            "auth to be set to username='******', password='******', mutual_username='******', "
            "mutual_password='******'".format(username, password, mutual_username,
                                          mutual_password))
        target_iqn = self.name

        api_vars = {
            "username": username,
            "password": password,
            "mutual_username": mutual_username,
            "mutual_password": mutual_password
        }

        targetauth_api = ('{}://localhost:{}/api/'
                          'targetauth/{}'.format(self.http_mode,
                                                 settings.config.api_port,
                                                 target_iqn))
        api = APIRequest(targetauth_api, data=api_vars)
        api.put()

        if api.response.status_code == 200:
            self.logger.debug("- target credentials updated")
            self.auth['username'] = username
            self.auth['password'] = password
            self.auth['mutual_username'] = mutual_username
            self.auth['mutual_password'] = mutual_password
            self.logger.info('ok')

        else:
            self.logger.error("Failed to update target auth: "
                              "{}".format(
                                  response_message(api.response, self.logger)))
            return
Exemple #22
0
    def ui_command_discovery_auth(self,
                                  username=None,
                                  password=None,
                                  mutual_username=None,
                                  mutual_password=None):
        """
        Discovery authentication can be set to use CHAP/CHAP_MUTUAL by supplying
        username, password, mutual_username, mutual_password

        Specifying 'nochap' will remove discovery authentication.

        e.g.
        auth username=<user> password=<pass> mutual_username=<m_user> mutual_password=<m_pass>

        """

        self.logger.warn(
            "discovery username={}, password={}, mutual_username={}, "
            "mutual_password={}".format(username, password, mutual_username,
                                        mutual_password))

        self.logger.debug("CMD: /iscsi discovery_auth")

        if not username:
            self.logger.error(
                "To set or reset discovery authentication, specify either "
                "username=<user> password=<password> [mutual_username]=<user> "
                "[mutual_password]=<password> or nochap")
            return

        if username == 'nochap':
            username = ''
            password = ''
            mutual_username = ''
            mutual_password = ''

        self.logger.debug(
            "discovery auth to be set to username='******', password='******', "
            "mutual_username='******', mutual_password='******'".format(
                username, password, mutual_username, mutual_password))

        api_vars = {
            "username": username,
            "password": password,
            "mutual_username": mutual_username,
            "mutual_password": mutual_password
        }
        discoveryauth_api = ('{}://localhost:{}/api/'
                             'discoveryauth'.format(self.http_mode,
                                                    settings.config.api_port))
        api = APIRequest(discoveryauth_api, data=api_vars)
        api.put()

        if api.response.status_code == 200:
            self._set_auth(username, password, mutual_username,
                           mutual_password)
            self.logger.info('ok')
        else:
            self.logger.error("Error: {}".format(
                response_message(api.response, self.logger)))
            return
Exemple #23
0
    def ui_command_host(self, action, client_iqn):
        """
        use the 'host' sub-command to add and remove hosts from a host group.
        Adding a host will automatically map the host group's disks to that
        specific host. Removing a host however, does not change the hosts
        disk masking - it simply removes the host from group.

        e.g.
        host add|remove iqn.1994-05.com.redhat:rh7-client
        """

        if action not in HostGroup.valid_actions:
            self.logger.error("Invalid request - must be "
                              "host add|remove <client_iqn>")
            return

        target_iqn = self.parent.parent.name

        # basic checks
        client_group = self._get_client_group(target_iqn)
        client_map = client_group.client_map
        if client_iqn not in client_map:
            self.logger.error("'{}' is not managed by a "
                              "group".format(client_iqn))
            return

        current_group = client_map[client_iqn].group_name
        if action == 'add' and current_group:
            self.logger.error("'{}' already belongs to "
                              "'{}'".format(client_iqn, current_group))
            return
        elif action == 'remove' and current_group != self.name:
            self.logger.error("'{}' does not belong to this "
                              "group".format(client_iqn))
            return

        # Basic checks passed, hand-off to the API now
        group_api = ('{}://{}:{}/api/hostgroup/'
                     '{}/{}'.format(self.http_mode, "localhost",
                                    settings.config.api_port, target_iqn,
                                    self.name))

        api_vars = {"action": action, "members": client_iqn}

        api = APIRequest(group_api, data=api_vars)
        api.put()
        self.logger.debug("- api call responded "
                          "{}".format(api.response.status_code))
        if api.response.status_code != 200:
            self.logger.error("Failed :"
                              "{}".format(
                                  response_message(api.response, self.logger)))
            return

        # group updated, so update the UI
        self.logger.debug("Updating the UI")
        if action == 'add':
            HostGroupMember(self, 'host', client_iqn)
            self.update_clients_UI([client_iqn], target_iqn)

        elif action == 'remove':
            child = [
                child for child in self.children if child.name == client_iqn
            ][0]
            self.delete(child)

        self.logger.info('ok')
Exemple #24
0
    def ui_command_create(self, gateway_name, ip_address, nosync=False,
                          skipchecks='false'):
        """
        Define a gateway to the gateway group for this iscsi target. The
        first host added should be the gateway running the command

        gateway_name ... should resolve to the hostname of the gateway
        ip_address ..... is the IPv4/IPv6 address of the interface the iscsi
                         portal should use
        nosync ......... by default new gateways are sync'd with the
                         existing configuration by cli. By specifying nosync
                         the sync step is bypassed - so the new gateway
                         will need to have it's rbd-target-gw daemon
                         restarted to apply the current configuration
                         (default = False)
        skipchecks ..... set this to true to force gateway validity checks
                         to be bypassed(default = False). This is a developer
                         option ONLY. Skipping these checks has the potential
                         to result in an unstable configuration.
        """

        ip_address = normalize_ip_address(ip_address)
        self.logger.debug("CMD: ../gateways/ create {} {} "
                          "nosync={} skipchecks={}".format(gateway_name,
                                                           ip_address,
                                                           nosync,
                                                           skipchecks))

        local_gw = this_host()
        current_gateways = [tgt.name for tgt in self.children]

        if gateway_name != local_gw and len(current_gateways) == 0:
            # the first gateway defined must be the local machine. By doing
            # this the initial create uses localhost, and places it's portal IP
            # in the gateway ip list. Once the gateway ip list is defined, the
            # api server can resolve against the gateways - until the list is
            # defined only a request from localhost is acceptable to the api
            self.logger.error("The first gateway defined must be the local "
                              "machine")
            return

        if skipchecks not in ['true', 'false']:
            self.logger.error("skipchecks must be either true or false")
            return

        if local_gw in current_gateways:
            current_gateways.remove(local_gw)

        config = self.parent.parent.parent._get_config()
        if not config:
            self.logger.error("Unable to refresh local config"
                              " over API - sync aborted, restart rbd-target-gw"
                              " on {} to sync".format(gateway_name))

        target_iqn = self.parent.name
        target_config = config['targets'][target_iqn]
        if nosync:
            sync_text = "sync skipped"
        else:
            sync_text = ("sync'ing {} disk(s) and "
                         "{} client(s)".format(len(target_config['disks']),
                                               len(target_config['clients'])))
        if skipchecks == 'true':
            self.logger.warning("OS version/package checks have been bypassed")

        self.logger.info("Adding gateway, {}".format(sync_text))

        gw_api = '{}://{}:{}/api'.format(self.http_mode,
                                         "localhost",
                                         settings.config.api_port)
        gw_rqst = gw_api + '/gateway/{}/{}'.format(target_iqn, gateway_name)
        gw_vars = {"nosync": nosync,
                   "skipchecks": skipchecks,
                   "ip_address": ip_address}

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

        msg = response_message(api.response, self.logger)
        if api.response.status_code != 200:
            self.logger.error("Failed : {}".format(msg))
            return

        self.logger.debug("{}".format(msg))
        self.logger.debug("Adding gw to UI")

        # Target created OK, get the details back from the gateway and
        # add to the UI. We have to use the new gateway to ensure what
        # we get back is current (the other gateways will lag until they see
        # epoch xattr change on the config object)
        new_gw_endpoint = ('{}://{}:{}/'
                           'api'.format(self.http_mode,
                                        gateway_name,
                                        settings.config.api_port))

        config = self.parent.parent.parent._get_config(endpoint=new_gw_endpoint)
        target_config = config['targets'][target_iqn]
        portal_config = target_config['portals'][gateway_name]
        Gateway(self, gateway_name, portal_config)

        self.logger.info('ok')
Exemple #25
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>

        e.g.
        auth chap=username/password | nochap

        username ... the username is 8-64 character string. Each character
                     may either be an alphanumeric or use one of the following
                     special characters .,:,-,@.
                     Consider using the hosts 'shortname' or the initiators IQN
                     value as the username

        password ... the password must be between 12-16 chars in length
                     containing alphanumeric characters, plus the following
                     special characters @,_,-

        WARNING: Using unsupported special characters may result in truncation,
                 resulting in failed logins.


        Specifying 'nochap' will remove chap authentication for the client
        across all gateways.

        """

        self.logger.debug("CMD: ../hosts/<client_iqn> auth *")

        if not chap:
            self.logger.error("To set or reset authentication, specify either "
                              "chap=<user>/<password> or nochap")
            return

        if chap == 'nochap':
            chap = ''
        else:
            # string could have been supplied as chap=user/password or
            # simply user/password - either way all we see is user/password
            if '/' not in chap:
                self.logger.error(
                    "CHAP format is invalid - must be a <username>/<password> "
                    "format. Use 'help auth' to show the correct syntax and "
                    "supported characters")
                return

        self.logger.debug(
            "CHAP to be set to '{}' for '{}'".format(chap, self.client_iqn))

        api_vars = {"chap": chap}

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

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

        if api.response.status_code == 200:
            self.logger.debug("- client credentials updated")
            self.auth['chap'] = chap
            self.logger.info('ok')

        else:
            self.logger.error("Failed to update the client's auth"
                              " :{}".format(response_message(api.response,
                                                             self.logger)))
            return
Exemple #26
0
    def ui_command_disk(self, action, disk_name):
        """
        use the 'disk' sub-command to add or remove a disk from a specific
        host group. Removing disks should be done with care, as the remove
        operation will be executed across all hosts defined to the host group.

        e.g.
        disk add|remove rbd.disk_1
        """

        if action not in HostGroup.valid_actions:
            self.logger.error("Invalid request - must be "
                              "disk add|remove <disk_image>")
            return

        target_iqn = self.parent.parent.name

        # simple sanity checks
        # 1. does the disk exist in the configuration
        ui_root = self.get_ui_root()
        all_pools = ui_root.disks.children
        all_disks = []
        for current_pool in all_pools:
            for current_disk in current_pool.children:
                all_disks.append(current_disk)
        if disk_name not in [disk.image_id for disk in all_disks]:
            self.logger.error("Disk '{}' is not defined within the "
                              "configuration".format(disk_name))
            return

        # 2. For an 'add' request, the disk must not already be in the host
        # group. Whereas, for a remove request the disk must exist.
        if action == 'add':
            if disk_name in self.disks:
                self.logger.error("'{}' is already defined to this "
                                  "host-group".format(disk_name))
                return
        else:
            if disk_name not in self.disks:
                self.logger.error("'{}' is not a member of this "
                                  "group".format(disk_name))
                return

        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)
            if rc == 0:
                self.logger.debug("disk auto-map successful")
            else:
                self.logger.error("disk auto-map failed({}), try "
                                  "using the /iscsi-target/<iqn>/disks add "
                                  "command".format(rc))
                return

        # Basic checks passed, hand-off to the API
        group_api = ('{}://{}:{}/api/hostgroup/'
                     '{}/{}'.format(self.http_mode, "localhost",
                                    settings.config.api_port, target_iqn,
                                    self.name))

        api_vars = {"action": action, "disks": disk_name}

        api = APIRequest(group_api, data=api_vars)
        api.put()
        self.logger.debug("- api call responded {}".format(
            api.response.status_code))
        if api.response.status_code != 200:
            self.logger.error("Failed: "
                              "{}".format(
                                  response_message(api.response, self.logger)))
            return

        # group updated, so update the host-groups UI elements
        self.logger.debug("Updating the UI")
        if action == 'add':
            HostGroupMember(self, 'disk', disk_name)
        elif action == 'remove':
            child = [
                child for child in self.children if child.name == disk_name
            ][0]
            self.delete(child)

        self.update_clients_UI(self.members, target_iqn)

        self.logger.info('ok')
Exemple #27
0
    def set_auth(self,
                 username=None,
                 password=None,
                 mutual_username=None,
                 mutual_password=None):

        self.logger.debug("username={}, password={}, mutual_username={}, "
                          "mutual_password={}".format(username, password,
                                                      mutual_username,
                                                      mutual_password))

        self.logger.debug("CMD: ../hosts/<client_iqn> auth *")

        if not username:
            self.logger.error(
                "To set or reset authentication, specify either "
                "username=<user> password=<password> "
                "[mutual_username]=<user> [mutual_password]=<password> "
                "or nochap")
            return

        if username == 'nochap':
            username = ''
            password = ''
            mutual_username = ''
            mutual_password = ''

        self.logger.debug(
            "auth to be set to username='******', password='******', mutual_username='******', "
            "mutual_password='******' for '{}'".format(username, password,
                                                   mutual_username,
                                                   mutual_password,
                                                   self.client_iqn))

        target_iqn = self.parent.parent.name

        api_vars = {
            "username": username,
            "password": password,
            "mutual_username": mutual_username,
            "mutual_password": mutual_password
        }

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

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

        if api.response.status_code == 200:
            self.logger.debug("- client credentials updated")
            self.auth['username'] = username
            self.auth['password'] = password
            self.auth['mutual_username'] = mutual_username
            self.auth['mutual_password'] = mutual_password
            self.logger.info('ok')

        else:
            self.logger.error("Failed to update the client's auth: "
                              "{}".format(
                                  response_message(api.response, self.logger)))
            return
Exemple #28
0
    def ui_command_create(self, gateway_name, ip_address, nosync=False):
        """
        Define a gateway to the gateway group for this iscsi target. The
        first host added should be the gateway running the command

        gateway_name ... should resolve to the hostname of the gateway
        ip_address ..... is the IP v4 address of the interface the iscsi
                         portal should use
        nosync ......... by default new gateways are sync'd with the
                         existing configuration by cli. By specifying nosync
                         the sync step is bypassed - so the new gateway
                         will need to have it's rbd-target-gw daemon
                         restarted to apply the current configuration
        """

        self.logger.debug("CMD: ../gateways/ create {} {} "
                          "nosync={}".format(gateway_name, ip_address, nosync))

        local_gw = this_host()
        current_gateways = [tgt.name for tgt in self.children]

        if gateway_name != local_gw and len(current_gateways) == 0:
            # the first gateway defined must be the local machine. By doing
            # this the initial create uses 127.0.0.1, and places it's portal IP
            # in the gateway ip list. Once the gateway ip list is defined, the
            # api server can resolve against the gateways - until the list is
            # defined only a request from 127.0.0.1 is acceptable to the api
            self.logger.error("The first gateway defined must be the local "
                              "machine")
            return

        if local_gw in current_gateways:
            current_gateways.remove(local_gw)

        config = self.parent.parent.parent._get_config()
        if not config:
            self.logger.error("Unable to refresh local config"
                              " over API - sync aborted, restart rbd-target-gw"
                              " on {} to sync".format(gateway_name))

        if nosync:
            sync_text = "sync skipped"
        else:
            sync_text = ("sync'ing {} disk(s) and "
                         "{} client(s)".format(len(config['disks']),
                                               len(config['clients'])))

        self.logger.info("Adding gateway, {}".format(sync_text))

        gw_api = '{}://{}:{}/api'.format(self.http_mode, "127.0.0.1",
                                         settings.config.api_port)
        gw_rqst = gw_api + '/gateway/{}'.format(gateway_name)
        gw_vars = {"nosync": nosync, "ip_address": ip_address}

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

        if api.response.status_code != 200:

            msg = api.response.json()['message']

            self.logger.error("Failed : {}".format(msg))
            return

        self.logger.debug("{}".format(api.response.json()['message']))
        self.logger.debug("Adding gw to UI")

        # Target created OK, get the details back from the gateway and
        # add to the UI. We have to use the new gateway to ensure what
        # we get back is current (the other gateways will lag until they see
        # epoch xattr change on the config object)
        new_gw_endpoint = ('{}://{}:{}/'
                           'api'.format(self.http_mode, gateway_name,
                                        settings.config.api_port))

        config = self.parent.parent.parent._get_config(
            endpoint=new_gw_endpoint)
        gw_config = config['gateways'][gateway_name]
        Gateway(self, gateway_name, gw_config)

        self.logger.info('ok')
Exemple #29
0
    def ui_command_disk(self, action='add', disk=None, size=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

        """

        self.logger.debug("CMD: ../hosts/<client_iqn> disk action={}"
                          " disk={}".format(action,
                                           disk))

        valid_actions = ['add', 'remove']

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

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

        lun_list = [(lun.rbd_name, lun.lun_id) for lun in self.children]
        current_luns = Client.get_srtd_names(lun_list)

        if action == 'add':

            if disk not in current_luns:
                ui_root = self.get_ui_root()
                valid_disk_names = [defined_disk.image_id
                                    for defined_disk in ui_root.disks.children]
            else:
                # disk provided is already mapped, so remind the user
                self.logger.error("Disk {} already mapped".format(disk))
                return
        else:
            valid_disk_names = current_luns

        if disk not in valid_disk_names:

            # if this is an add operation, we can create the disk on-the-fly
            # for the admin

            if action == 'add':
                ui_root = self.get_ui_root()
                ui_disks = ui_root.disks
                if not size:
                    self.logger.error("To auto-define the disk to the client"
                                      " you must provide a disk size")
                    return

                # a disk given here would be of the form pool.image
                pool, image = disk.split('.')
                rc = ui_disks.create_disk(pool=pool, image=image, size=size)
                if rc == 0:
                    self.logger.debug("disk auto-define successful")
                else:
                    self.logger.error("disk auto-define failed({}), try "
                                      "using the /disks create "
                                      "command".format(rc))
                    return

            else:
                self.logger.error("disk '{}' is not mapped to this "
                                     "client ".format(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))

        api_vars = {"disk": disk}

        # if action == 'add':
        #     current_luns.append(disk)
        # else:
        #     current_luns.remove(disk)
        #
        # image_list = ','.join(current_luns)
        #
        # api_vars = {"image_list": image_list,
        #             "chap": self.auth.get('chap', '')}

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

        api = APIRequest(clientlun_api, data=api_vars)
        if action == 'add':
            api.put()
        else:
            api.delete()

        if api.response.status_code == 200:

            self.logger.debug("disk mapping updated successfully")

            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}
                clientlun_api = clientlun_api.replace('/all_clientlun/',
                                                      '/clientlun/')
                self.logger.debug("Querying API to get mapped LUN information")
                api = APIRequest(clientlun_api, data=get_api_vars)
                api.get()

                if api.response.status_code == 200:
                    lun_dict = api.response.json()['message']
                    lun_id = lun_dict[disk]['lun_id']
                    MappedLun(self, disk, lun_id)

                    # update the objects lun list (so ui info cmd picks
                    # up the change
                    self.luns[disk] = {'lun_id': lun_id}
                    self.parent.update_lun_map('add', disk, self.client_iqn)
                    active_maps = len(self.parent.lun_map[disk]) - 1
                    if active_maps > 0:
                        self.logger.warning("Warning: '{}' mapped to {} other "
                                            "client(s)".format(disk,
                                                               active_maps))

                else:
                    self.logger.error("Query for disk '{}' meta data "
                                      "failed".format(disk))
                    return

            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)
                del self.luns[disk]
                self.parent.update_lun_map('remove', disk, self.client_iqn)


            self.logger.debug("configuration update successful")
            self.logger.info('ok')

        else:
            # the request to add/remove the disk for the client failed
            self.logger.error("disk {} for '{}' against {} "
                              "failed\n{}".format(action,
                                              disk,
                                              self.client_iqn,
                                              api.response.json()['message']))
            return