Beispiel #1
0
class Node(csi.NodeServicer, Identity):
    STAGE_RESP = types.StageResp()
    UNSTAGE_RESP = types.UnstageResp()
    NODE_PUBLISH_RESP = types.NodePublishResp()
    NODE_UNPUBLISH_RESP = types.NodeUnpublishResp()

    def __init__(self,
                 server,
                 persistence_config=None,
                 cinderlib_config=None,
                 node_id=None,
                 storage_nw_ip=None,
                 **kwargs):
        if persistence_config:
            cinderlib_config['fail_on_missing_backend'] = False
            cinderlib.setup(persistence_config=persistence_config,
                            **cinderlib_config)
            Identity.__init__(self, server, cinderlib_config)

        node_id = node_id or socket.getfqdn()
        self.node_id = types.IdResp(node_id=node_id)
        self.node_info = NodeInfo.set(node_id, storage_nw_ip)
        csi.add_NodeServicer_to_server(self, server)

    def _get_split_file(self, filename):
        with open(filename) as f:
            result = [line.split() for line in f.read().split('\n') if line]
        return result

    def _get_mountinfo(self):
        return self._get_split_file('/proc/self/mountinfo')

    def _vol_private_location(self, volume_id):
        private_bind = os.path.join(os.getcwd(), volume_id)
        return private_bind

    def _get_mount(self, private_bind):
        mounts = self._get_split_file('/proc/self/mounts')
        result = [mount for mount in mounts if mount[0] == private_bind]
        return result

    def _get_device(self, path):
        for line in self._get_mountinfo():
            if line[4] == path:
                return line[9] if line[9].startswith('/') else line[3]
        return None

    def _get_vol_device(self, volume_id):
        private_bind = self._vol_private_location(volume_id)
        device = self._get_device(private_bind)
        return device, private_bind

    def _format_device(self, fs_type, device, context):
        # We don't use the util-linux Python library to reduce dependencies
        stdout, stderr = self.sudo('lsblk',
                                   '-nlfoFSTYPE',
                                   device,
                                   retries=5,
                                   errors=[1, 32],
                                   delay=2)
        fs_types = filter(None, stdout.split())
        if fs_types:
            if fs_types[0] == fs_type:
                return
            context.abort(
                grpc.StatusCode.ALREADY_EXISTS,
                'Cannot stage filesystem %s on device that '
                'already has filesystem %s' % (fs_type, fs_types[0]))
        cmd = [self.MKFS + fs_type]
        cmd.extend(self.MKFS_ARGS.get(fs_type, self.DEFAULT_MKFS_ARGS))
        cmd.append(device)
        self.sudo(*cmd)

    def _check_mount_exists(self, capability, private_bind, target, context):
        mounts = self._get_mount(private_bind)
        if mounts:
            if target != mounts[0][1]:
                context.abort(
                    grpc.StatusCode.ALREADY_EXISTS,
                    'Filesystem already mounted on %s' % mounts[0][1])

            requested_flags = set(capability.mount.mount_flags or [])
            missing_flags = requested_flags.difference(mounts[0][3].split(','))
            if missing_flags:
                context.abort(
                    grpc.StatusCode.ALREADY_EXISTS,
                    'Already mounted with different flags (%s)' %
                    missing_flags)
            return True
        return False

    def _mount(self, fs_type, mount_flags, private_bind, target):
        # Mount must only be called if it's already not mounted
        # We don't use the util-linux Python library to reduce dependencies
        command = ['mount', '-t', fs_type]
        if mount_flags:
            command.append('-o')
            command.append(','.join(mount_flags))
        command.append(private_bind)
        command.append(target)
        self.sudo(*command)

    def _check_path(self, request, context, is_staging):
        is_block = request.volume_capability.HasField('block')
        attr_name = 'staging_target_path' if is_staging else 'target_path'
        path = getattr(request, attr_name)
        try:
            st_mode = os.stat(path).st_mode
        except OSError as exc:
            context.abort(grpc.StatusCode.INVALID_ARGUMENT,
                          'Invalid %s path: %s' % (attr_name, exc))

        if ((is_block and stat.S_ISBLK(st_mode) or stat.S_ISREG(st_mode))
                or (not is_block and stat.S_ISDIR(st_mode))):
            return path, is_block

        context.abort(grpc.StatusCode.INVALID_ARGUMENT,
                      'Invalid existing %s' % attr_name)

    @debuggable
    @logrpc
    @require('volume_id', 'staging_target_path', 'volume_capability')
    @Worker.unique
    def NodeStageVolume(self, request, context):
        vol = self._get_vol(request.volume_id)
        if not vol:
            context.abort(grpc.StatusCode.NOT_FOUND,
                          'Volume %s does not exist' % request.volume_id)

        self._validate_capabilities([request.volume_capability], context)
        target, is_block = self._check_path(request, context, is_staging=True)

        device, private_bind = self._get_vol_device(vol.id)
        if not device:
            # For now we don't really require the publish_info, since we share
            # the persistence storage, but if we would need to deserialize it
            # with json.loads from key 'connection_info'
            conn = vol.connections[0]
            # Some slow systems may take a while to detect the multipath so we
            # retry the attach.  Since we don't disconnect this will go fast
            # through the login phase.
            for i in range(MULTIPATH_FIND_RETRIES):
                conn.attach()
                if not conn.use_multipath or conn.path.startswith('/dev/dm'):
                    break
                sys.stdout.write('Retrying to get a multipath\n')
            # Create the private bind file
            open(private_bind, 'a').close()
            # TODO(geguileo): make path for private binds configurable
            self.sudo('mount', '--bind', conn.path, private_bind)
            device = conn.path

        if is_block:
            # Avoid multiple binds if CO incorrectly called us twice
            device = self._get_device(target)
            if not device:
                # TODO(geguileo): Add support for NFS/QCOW2
                self.sudo('mount', '--bind', private_bind, target)
        else:
            if not self._check_mount_exists(request.volume_capability,
                                            private_bind, target, context):
                fs_type = (request.volume_capability.mount.fs_type
                           or DEFAULT_MOUNT_FS)
                self._format_device(fs_type, private_bind, context)
                self._mount(fs_type,
                            request.volume_capability.mount.mount_flags,
                            private_bind, target)
        return self.STAGE_RESP

    @debuggable
    @logrpc
    @require('volume_id', 'staging_target_path')
    @Worker.unique
    def NodeUnstageVolume(self, request, context):
        # TODO(geguileo): Add support for NFS/QCOW2
        vol = self._get_vol(request.volume_id)
        if not vol:
            context.abort(grpc.StatusCode.NOT_FOUND,
                          'Volume %s does not exist' % request.volume_id)

        device, private_bind = self._get_vol_device(vol.id)
        # If it's not already unstaged
        if device:
            count = 0
            for line in self._get_mountinfo():
                if line[3] in (device, private_bind):
                    count += 1

            if self._get_mount(private_bind):
                count += 1

            # If the volume is still in use we cannot unstage (one use is for
            # our private volume reference and the other for staging path
            if count > 2:
                context.abort(grpc.StatusCode.ABORTED,
                              'Operation pending for volume')

            if count == 2:
                self.sudo('umount', request.staging_target_path, retries=4)
            if count > 0:
                self.sudo('umount', private_bind, retries=4)
            os.remove(private_bind)

            conn = vol.connections[0]
            conn.detach()

        return self.UNSTAGE_RESP

    @debuggable
    @logrpc
    @require('volume_id', 'staging_target_path', 'target_path',
             'volume_capability')
    @Worker.unique
    def NodePublishVolume(self, request, context):
        self._validate_capabilities([request.volume_capability], context)
        staging_target, is_block = self._check_path(request,
                                                    context,
                                                    is_staging=True)

        device, private_bind = self._get_vol_device(request.volume_id)
        error = (not device
                 or (is_block and not self._get_device(staging_target))
                 or (not is_block and not self._check_mount_exists(
                     request.volume_capability, private_bind, staging_target,
                     context)))
        if error:
            context.abort(grpc.StatusCode.FAILED_PRECONDITION,
                          'Staging was not been successfully called')

        target, is_block = self._check_path(request, context, is_staging=False)

        # TODO(geguileo): Add support for modes, etc.

        # Check if it's already published
        device = self._get_device(target)
        volume_device, private_bind = self._get_vol_device(request.volume_id)
        if device in (volume_device, staging_target, private_bind):
            return self.NODE_PUBLISH_RESP

        # TODO(geguileo): Check how many are mounted and fail if > 0

        # If not published bind it
        self.sudo('mount', '--bind', staging_target, target)
        return self.NODE_PUBLISH_RESP

    @debuggable
    @logrpc
    @require('volume_id', 'target_path')
    @Worker.unique
    def NodeUnpublishVolume(self, request, context):
        device = self._get_device(request.target_path)
        if device:
            self.sudo('umount', request.target_path, retries=4)
        return self.NODE_UNPUBLISH_RESP

    @debuggable
    @logrpc
    def NodeGetId(self, request, context):
        return self.node_id

    @debuggable
    @logrpc
    def NodeGetCapabilities(self, request, context):
        rpc = types.NodeCapabilityType.STAGE_UNSTAGE_VOLUME
        capabilities = [types.NodeCapability(rpc=types.NodeRPC(type=rpc))]
        return types.NodeCapabilityResp(capabilities=capabilities)
Beispiel #2
0
class Node(csi.NodeServicer, Identity):
    STAGE_RESP = types.StageResp()
    UNSTAGE_RESP = types.UnstageResp()
    NODE_PUBLISH_RESP = types.NodePublishResp()
    NODE_UNPUBLISH_RESP = types.NodeUnpublishResp()

    def __init__(self,
                 server,
                 persistence_config=None,
                 node_id=None,
                 storage_nw_ip=None,
                 **kwargs):
        if persistence_config:
            self.persistence = persistence.setup(persistence_config)
            # TODO(geguileo): Make Node only service work, which may require
            # modifications to cinderlib or faking the Backend object, since
            # all objects set the backend field on initialization.
            # cinderlib.objects.Object.setup(self.persistence, ...)

        node_id = node_id or socket.getfqdn()
        self.node_id = types.IdResp(node_id=node_id)
        self.node_info = NodeInfo.set(node_id, storage_nw_ip)
        Identity.__init__(self, server)
        csi.add_NodeServicer_to_server(self, server)

    def _get_mountinfo(self):
        with open('/proc/self/mountinfo') as f:
            mountinfo = [line.split() for line in f.read().split('\n') if line]
        return mountinfo

    def _vol_private_location(self, volume_id):
        private_bind = os.path.join(os.getcwd(), volume_id)
        return private_bind

    def _get_device(self, path):
        for line in self._get_mountinfo():
            if line[4] == path:
                return line[3]
        return None

    def _get_vol_device(self, volume_id):
        private_bind = self._vol_private_location(volume_id)
        device = self._get_device(private_bind)
        return device, private_bind

    def NodeStageVolume(self, request, context):
        vol = self._get_vol(request.volume_id)
        if not vol:
            context.abort(grpc.StatusCode.NOT_FOUND,
                          'Volume %s does not exist' % request.volume_id)

        # TODO(geguileo): Check capabilities
        # TODO(geguileo): Check that the provided staging_target_path is an
        # existing file for block or directory for filesystems
        device, private_bind = self._get_vol_device(vol.id)
        if not device:
            # For now we don't really require the publish_info, since we share
            # the persistence storage, but if we would need to deserialize it
            # with json.loads from key 'connection_info'
            conn = vol.connections[0]
            conn.attach()
            # Create the private bind file
            open(private_bind, 'a').close()
            # TODO(geguileo): make path for private binds configurable
            self.sudo('mount', '--bind', conn.path, private_bind)

        # If CO did something wrong and called us twice avoid multiple binds
        device = self._get_device(request.staging_target_path)
        if not device:
            # TODO(geguileo): Add support for NFS/QCOW2
            self.sudo('mount', '--bind', private_bind,
                      request.staging_target_path)
        return self.STAGE_RESP

    def NodeUnstageVolume(self, request, context):
        # TODO(geguileo): Add support for NFS/QCOW2
        vol = self._get_vol(request.volume_id)
        if not vol:
            context.abort(grpc.StatusCode.NOT_FOUND,
                          'Volume %s does not exist' % request.volume_id)

        device, private_bind = self._get_vol_device(vol.id)
        # If it's not already unstaged
        if device:
            count = 0
            for line in self._get_mountinfo():
                if line[3] in (device, private_bind):
                    count += 1

            # If the volume is still in use we cannot unstage (one use is for
            # our private volume reference and the other for staging path
            if count > 2:
                context.abort(grpc.StatusCode.ABORTED,
                              'Operation pending for volume')

            conn = vol.connections[0]
            if count == 2:
                self.sudo('umount', request.staging_target_path)
            conn.detach()
            if count > 0:
                self.sudo('umount', private_bind)
            os.remove(private_bind)
        return self.UNSTAGE_RESP

    def NodePublishVolume(self, request, context):

        # TODO(geguileo): Check if staging_target_path is passed and exists
        # TODO(geguileo): Add support for modes, etc.
        # Check if it's already published
        device = self._get_device(request.target_path)
        volume_device, private_bind = self._get_vol_device(request.volume_id)
        if device in (volume_device, request.staging_target_path):
            return self.NODE_PUBLISH_RESP

        # TODO(geguileo): Check how many are mounted and fail if > 0

        # If not published bind it
        self.sudo('mount', '--bind', request.staging_target_path,
                  request.target_path)
        return self.NODE_PUBLISH_RESP

    def NodeUnpublishVolume(self, request, context):
        device = self._get_device(request.target_path)
        if device:
            self.sudo('umount', request.target_path)
        return self.NODE_UNPUBLISH_RESP

    def NodeGetId(self, request, context):
        return self.node_id

    def NodeGetCapabilities(self, request, context):
        rpc = types.NodeCapabilityType.STAGE_UNSTAGE_VOLUME
        capabilities = [types.NodeCapability(rpc=types.NodeRPC(type=rpc))]
        return types.NodeCapabilityResp(capabilities=capabilities)