<commit_before># -*- test-case-name: flocker.node.agents.test.test_blockdevice -*-
# Copyright Hybrid Logic Ltd.  See LICENSE file for details.

"""
This module implements the parts of a block-device based dataset
convergence agent that can be re-used against many different kinds of block
devices.
"""

from uuid import UUID
from subprocess import check_output
from functools import wraps

from eliot import ActionType, Field, Logger
from eliot.serializers import identity

from zope.interface import implementer, Interface

from pyrsistent import PRecord, field

import psutil

from twisted.internet.defer import succeed
from twisted.python.filepath import FilePath

from .. import IDeployer, IStateChange, Sequentially, InParallel
from ...control import NodeState, Manifestation, Dataset, NonManifestDatasets

# Eliot is transitioning away from the "Logger instances all over the place"
# approach.  And it's hard to put Logger instances on PRecord subclasses which
# we have a lot of.  So just use this global logger for now.
_logger = Logger()


class VolumeException(Exception):
    """
    A base class for exceptions raised by  ``IBlockDeviceAPI`` operations.

    :param unicode blockdevice_id: The unique identifier of the block device.
    """
    def __init__(self, blockdevice_id):
        if not isinstance(blockdevice_id, unicode):
            raise TypeError(
                'Unexpected blockdevice_id type. '
                'Expected unicode. '
                'Got {!r}.'.format(blockdevice_id)
            )
        Exception.__init__(self, blockdevice_id)
        self.blockdevice_id = blockdevice_id


class UnknownVolume(VolumeException):
    """
    The block device could not be found.
    """


class AlreadyAttachedVolume(VolumeException):
    """
    A failed attempt to attach a block device that is already attached.
    """


class UnattachedVolume(VolumeException):
    """
    An attempt was made to operate on an unattached volume but the operation
    requires the volume to be attached.
    """


DATASET = Field(
    u"dataset",
    lambda dataset: dataset.dataset_id,
    u"The unique identifier of a dataset."
)

VOLUME = Field(
    u"volume",
    lambda volume: volume.blockdevice_id,
    u"The unique identifier of a volume."
)

DATASET_ID = Field(
    u"dataset_id",
    lambda dataset_id: unicode(dataset_id),
    u"The unique identifier of a dataset."
)

MOUNTPOINT = Field(
    u"mountpoint",
    lambda path: path.path,
    u"The absolute path to the location on the node where the dataset will be "
    u"mounted.",
)

DEVICE_PATH = Field(
    u"block_device_path",
    lambda path: path.path,
    u"The absolute path to the block device file on the node where the "
    u"dataset is attached.",
)

BLOCK_DEVICE_ID = Field(
    u"block_device_id",
    lambda id: unicode(id),
    u"The unique identifier if the underlying block device."
)

BLOCK_DEVICE_SIZE = Field(
    u"block_device_size",
    identity,
    u"The size of the underlying block device."
)

BLOCK_DEVICE_HOST = Field(
    u"block_device_host",
    identity,
    u"The host to which the underlying block device is attached."
)

CREATE_BLOCK_DEVICE_DATASET = ActionType(
    u"agent:blockdevice:create",
    [DATASET, MOUNTPOINT],
    [DEVICE_PATH, BLOCK_DEVICE_ID, DATASET_ID, BLOCK_DEVICE_SIZE,
     BLOCK_DEVICE_HOST],
    u"A block-device-backed dataset is being created.",
)

DESTROY_BLOCK_DEVICE_DATASET = ActionType(
    u"agent:blockdevice:destroy",
    [DATASET_ID],
    [],
    u"A block-device-backed dataset is being destroyed.",
)

UNMOUNT_BLOCK_DEVICE = ActionType(
    u"agent:blockdevice:unmount",
    [VOLUME],
    [],
    u"A block-device-backed dataset is being unmounted.",
)

DETACH_VOLUME = ActionType(
    u"agent:blockdevice:detach_volume",
    [VOLUME],
    [],
    u"The volume for a block-device-backed dataset is being detached."
)

DESTROY_VOLUME = ActionType(
    u"agent:blockdevice:destroy_volume",
    [VOLUME],
    [],
    u"The volume for a block-device-backed dataset is being destroyed."
)


def _logged_statechange(cls):
    """
    Decorate an ``IStateChange.run`` implementation with partially automatic
    logging.

    :param cls: An ``IStateChange`` implementation which also has an
        ``_eliot_action`` attribute giving an Eliot action that should be used
        to log its ``run`` method.

    :return: ``cls``, mutated so that its ``run`` method is automatically run
        in the context of its ``_eliot_action``.
    """
    original_run = cls.run
    # Work-around https://twistedmatrix.com/trac/ticket/7832
    try:
        original_run.__name__ = original_run.methodName
    except AttributeError:
        pass

    @wraps(original_run)
    def run(self, deployer):
        with self._eliot_action:
            # IStateChange.run nominally returns a Deferred.  Hook it up to the
            # action properly.  Do this as part of FLOC-1549 or maybe earlier.
            return original_run(self, deployer)

    cls.run = run
    return cls


class BlockDeviceVolume(PRecord):
    """
    A block device that may be attached to a host.

    :ivar unicode blockdevice_id: An identifier for the block device which is
        unique across the entire cluster.  For example, an EBS volume
        identifier (``vol-4282672b``).  This is used to address the block
        device for operations like attach and detach.
    :ivar int size: The size, in bytes, of the block device.
    :ivar unicode host: The IP address of the host to which the block device is
        attached or ``None`` if it is currently unattached.
    :ivar UUID dataset_id: The Flocker dataset ID associated with this volume.
    """
    blockdevice_id = field(type=unicode, mandatory=True)
    size = field(type=int, mandatory=True)
    host = field(type=(unicode, type(None)), initial=None)
    dataset_id = field(type=UUID, mandatory=True)


# Replace this with a simpler factory-function based API like:
#
#     change = destroy_blockdevice_dataset(volme)
#
# after FLOC-1591 makes it possible to have reasonable logging with such a
# solution.
@_logged_statechange
@implementer(IStateChange)
class DestroyBlockDeviceDataset(PRecord):
    """
    Destroy the volume for a dataset with a primary manifestation on the node
    where this state change runs.

    :ivar UUID dataset_id: The unique identifier of the dataset to which the
        volume to be destroyed belongs.
    """
    dataset_id = field(type=UUID, mandatory=True)

    @property
    def _eliot_action(self):
        return DESTROY_BLOCK_DEVICE_DATASET(
            _logger, dataset_id=self.dataset_id
        )

    def run(self, deployer):
        for volume in deployer.block_device_api.list_volumes():
            if volume.dataset_id == self.dataset_id:
                return Sequentially(
                    changes=[
                        UnmountBlockDevice(volume=volume),
                        DetachVolume(volume=volume),
                        DestroyVolume(volume=volume),
                    ]
                ).run(deployer)
        return succeed(None)


def _volume():
    """
    Create and return a ``PRecord`` ``field`` to hold a ``BlockDeviceVolume``.
    """
    return field(
        type=BlockDeviceVolume, mandatory=True,
        # Disable the automatic PRecord.create factory.  Callers can just
        # supply the right type, we don't need the magic coercion behavior
        # supplied by default.
        factory=lambda x: x
    )


@_logged_statechange
@implementer(IStateChange)
class UnmountBlockDevice(PRecord):
    """
    Unmount the filesystem mounted from the block device backed by a particular
    volume.

    :ivar BlockDeviceVolume volume: The volume associated with the dataset
        which will be unmounted.
    """
    volume = _volume()

    @property
    def _eliot_action(self):
        return UNMOUNT_BLOCK_DEVICE(_logger, volume=self.volume)

    def run(self, deployer):
        """
        Run the system ``unmount`` tool to unmount this change's volume's block
        device.  The volume must be attached to this node and the corresponding
        block device mounted.
        """
        device = deployer.block_device_api.get_device_path(
            self.volume.blockdevice_id
        )
        # This should be asynchronous.  Do it as part of FLOC-1499.  Make sure
        # to fix _logged_statechange to handle Deferreds too.
        check_output([b"umount", device.path])
        return succeed(None)


@_logged_statechange
@implementer(IStateChange)
class DetachVolume(PRecord):
    """
    Detach a volume from the node it is currently attached to.

    :ivar BlockDeviceVolume volume: The volume to destroy.
    """
    volume = _volume()

    @property
    def _eliot_action(self):
        return DETACH_VOLUME(_logger, volume=self.volume)

    def run(self, deployer):
        """
        Use the deployer's ``IBlockDeviceAPI`` to detach the volume.
        """
        # Make this asynchronous after FLOC-1549, probably as part of
        # FLOC-1593.
        deployer.block_device_api.detach_volume(self.volume.blockdevice_id)
        return succeed(None)


@_logged_statechange
@implementer(IStateChange)
class DestroyVolume(PRecord):
    """
    Destroy the storage (and therefore contents) of a volume.

    :ivar BlockDeviceVolume volume: The volume to destroy.
    """
    volume = _volume()

    @property
    def _eliot_action(self):
        return DESTROY_VOLUME(_logger, volume=self.volume)

    def run(self, deployer):
        """
        Use the deployer's ``IBlockDeviceAPI`` to destroy the volume.
        """
        # Make this asynchronous as part of FLOC-1549.
        deployer.block_device_api.destroy_volume(self.volume.blockdevice_id)
        return succeed(None)


@implementer(IStateChange)
class CreateBlockDeviceDataset(PRecord):
    """
    An operation to create a new dataset on a newly created volume with a newly
    initialized filesystem.

    :ivar Dataset dataset: The dataset for which to create a block device.
    :ivar FilePath mountpoint: The path at which to mount the created device.
    """
    dataset = field(mandatory=True, type=Dataset)
    mountpoint = field(mandatory=True, type=FilePath)

    def run(self, deployer):
        """
        Create a block device, attach it to the local host, create an ``ext4``
        filesystem on the device and mount it.

        Operations are performed synchronously.

        See ``IStateChange.run`` for general argument and return type
        documentation.

        :returns: An already fired ``Deferred`` with result ``None``.
        """
        with CREATE_BLOCK_DEVICE_DATASET(
                _logger,
                dataset=self.dataset, mountpoint=self.mountpoint
        ) as action:
            api = deployer.block_device_api
            volume = api.create_volume(
                dataset_id=UUID(self.dataset.dataset_id),
                size=self.dataset.maximum_size,
            )

            # This will be factored into a separate IStateChange to support the
            # case where the volume exists but is not attached.  That object
            # will be used by this one to perform this work.  FLOC-1575
            volume = api.attach_volume(
                volume.blockdevice_id, deployer.hostname
            )
            device = api.get_device_path(volume.blockdevice_id)

            # This will be factored into a separate IStateChange to support the
            # case where the volume is attached but has no filesystem.  That
            # object will be used by this one to perform this work. FLOC-1576
            check_output(["mkfs", "-t", "ext4", device.path])

            # This will be factored into a separate IStateChange to support the
            # case where the only state change necessary is mounting.  That
            # object will be used by this one to perform this mount. It will
            # also gracefully handle the case where the mountpoint directory
            # already exists.  FLOC-1498
            self.mountpoint.makedirs()
            check_output(["mount", device.path, self.mountpoint.path])

            action.add_success_fields(
                block_device_path=device,
                block_device_id=volume.blockdevice_id,
                dataset_id=volume.dataset_id,
                block_device_size=volume.size,
                block_device_host=volume.host,
            )
        return succeed(None)


# TODO: Introduce a non-blocking version of this interface and an automatic
# thread-based wrapper for adapting this to the other.  Use that interface
# anywhere being non-blocking is important (which is probably lots of places).
# See https://clusterhq.atlassian.net/browse/FLOC-1549
class IBlockDeviceAPI(Interface):
    """
    Common operations provided by all block device backends.

    Note: This is an early sketch of the interface and it'll be refined as we
    real blockdevice providers are implemented.
    """
    def create_volume(dataset_id, size):
        """
        Create a new volume.

        XXX: Probably needs to be some checking of valid sizes for different
        backends. Perhaps the allowed sizes should be defined as constants?

        :param UUID dataset_id: The Flocker dataset ID of the dataset on this
            volume.
        :param int size: The size of the new volume in bytes.
        :returns: A ``BlockDeviceVolume``.
        """

    def destroy_volume(blockdevice_id):
        """
        Destroy an existing volume.

        :param unicode blockdevice_id: The unique identifier for the volume to
            destroy.

        :raises UnknownVolume: If the supplied ``blockdevice_id`` does not
            exist.

        :return: ``None``
        """

    def attach_volume(blockdevice_id, host):
        """
        Attach ``blockdevice_id`` to ``host``.

        :param unicode blockdevice_id: The unique identifier for the block
            device being attached.
        :param unicode host: The IP address of a host to attach the volume to.
        :raises UnknownVolume: If the supplied ``blockdevice_id`` does not
            exist.
        :raises AlreadyAttachedVolume: If the supplied ``blockdevice_id`` is
            already attached.
        :returns: A ``BlockDeviceVolume`` with a ``host`` attribute set to
            ``host``.
        """

    def detach_volume(blockdevice_id):
        """
        Detach ``blockdevice_id`` from whatever host it is attached to.

        :param unicode blockdevice_id: The unique identifier for the block
            device being detached.

        :raises UnknownVolume: If the supplied ``blockdevice_id`` does not
            exist.
        :raises UnattachedVolume: If the supplied ``blockdevice_id`` is
            not attached to anything.
        :returns: ``None``
        """

    def resize_volume(blockdevice_id, size):
        """
        Resize an unattached ``blockdevice_id``.

        :param unicode blockdevice_id: The unique identifier for the block
            device being detached.

        :param int size: The required size, in bytes, of the volume.

        :raises UnknownVolume: If the supplied ``blockdevice_id`` does not
            exist.

        :returns: ``None``
        """

    def list_volumes():
        """
        List all the block devices available via the back end API.

        :returns: A ``list`` of ``BlockDeviceVolume``s.
        """

    def get_device_path(blockdevice_id):
        """<commit_msg>Note the scope of this method.<commit_after># -*- test-case-name: flocker.node.agents.test.test_blockdevice -*-
# Copyright Hybrid Logic Ltd.  See LICENSE file for details.

"""
This module implements the parts of a block-device based dataset
convergence agent that can be re-used against many different kinds of block
devices.
"""

from uuid import UUID
from subprocess import check_output
from functools import wraps

from eliot import ActionType, Field, Logger
from eliot.serializers import identity

from zope.interface import implementer, Interface

from pyrsistent import PRecord, field

import psutil

from twisted.internet.defer import succeed
from twisted.python.filepath import FilePath

from .. import IDeployer, IStateChange, Sequentially, InParallel
from ...control import NodeState, Manifestation, Dataset, NonManifestDatasets

# Eliot is transitioning away from the "Logger instances all over the place"
# approach.  And it's hard to put Logger instances on PRecord subclasses which
# we have a lot of.  So just use this global logger for now.
_logger = Logger()


class VolumeException(Exception):
    """
    A base class for exceptions raised by  ``IBlockDeviceAPI`` operations.

    :param unicode blockdevice_id: The unique identifier of the block device.
    """
    def __init__(self, blockdevice_id):
        if not isinstance(blockdevice_id, unicode):
            raise TypeError(
                'Unexpected blockdevice_id type. '
                'Expected unicode. '
                'Got {!r}.'.format(blockdevice_id)
            )
        Exception.__init__(self, blockdevice_id)
        self.blockdevice_id = blockdevice_id


class UnknownVolume(VolumeException):
    """
    The block device could not be found.
    """


class AlreadyAttachedVolume(VolumeException):
    """
    A failed attempt to attach a block device that is already attached.
    """


class UnattachedVolume(VolumeException):
    """
    An attempt was made to operate on an unattached volume but the operation
    requires the volume to be attached.
    """


DATASET = Field(
    u"dataset",
    lambda dataset: dataset.dataset_id,
    u"The unique identifier of a dataset."
)

VOLUME = Field(
    u"volume",
    lambda volume: volume.blockdevice_id,
    u"The unique identifier of a volume."
)

DATASET_ID = Field(
    u"dataset_id",
    lambda dataset_id: unicode(dataset_id),
    u"The unique identifier of a dataset."
)

MOUNTPOINT = Field(
    u"mountpoint",
    lambda path: path.path,
    u"The absolute path to the location on the node where the dataset will be "
    u"mounted.",
)

DEVICE_PATH = Field(
    u"block_device_path",
    lambda path: path.path,
    u"The absolute path to the block device file on the node where the "
    u"dataset is attached.",
)

BLOCK_DEVICE_ID = Field(
    u"block_device_id",
    lambda id: unicode(id),
    u"The unique identifier if the underlying block device."
)

BLOCK_DEVICE_SIZE = Field(
    u"block_device_size",
    identity,
    u"The size of the underlying block device."
)

BLOCK_DEVICE_HOST = Field(
    u"block_device_host",
    identity,
    u"The host to which the underlying block device is attached."
)

CREATE_BLOCK_DEVICE_DATASET = ActionType(
    u"agent:blockdevice:create",
    [DATASET, MOUNTPOINT],
    [DEVICE_PATH, BLOCK_DEVICE_ID, DATASET_ID, BLOCK_DEVICE_SIZE,
     BLOCK_DEVICE_HOST],
    u"A block-device-backed dataset is being created.",
)

DESTROY_BLOCK_DEVICE_DATASET = ActionType(
    u"agent:blockdevice:destroy",
    [DATASET_ID],
    [],
    u"A block-device-backed dataset is being destroyed.",
)

UNMOUNT_BLOCK_DEVICE = ActionType(
    u"agent:blockdevice:unmount",
    [VOLUME],
    [],
    u"A block-device-backed dataset is being unmounted.",
)

DETACH_VOLUME = ActionType(
    u"agent:blockdevice:detach_volume",
    [VOLUME],
    [],
    u"The volume for a block-device-backed dataset is being detached."
)

DESTROY_VOLUME = ActionType(
    u"agent:blockdevice:destroy_volume",
    [VOLUME],
    [],
    u"The volume for a block-device-backed dataset is being destroyed."
)


def _logged_statechange(cls):
    """
    Decorate an ``IStateChange.run`` implementation with partially automatic
    logging.

    :param cls: An ``IStateChange`` implementation which also has an
        ``_eliot_action`` attribute giving an Eliot action that should be used
        to log its ``run`` method.

    :return: ``cls``, mutated so that its ``run`` method is automatically run
        in the context of its ``_eliot_action``.
    """
    original_run = cls.run
    # Work-around https://twistedmatrix.com/trac/ticket/7832
    try:
        original_run.__name__ = original_run.methodName
    except AttributeError:
        pass

    @wraps(original_run)
    def run(self, deployer):
        with self._eliot_action:
            # IStateChange.run nominally returns a Deferred.  Hook it up to the
            # action properly.  Do this as part of FLOC-1549 or maybe earlier.
            return original_run(self, deployer)

    cls.run = run
    return cls


class BlockDeviceVolume(PRecord):
    """
    A block device that may be attached to a host.

    :ivar unicode blockdevice_id: An identifier for the block device which is
        unique across the entire cluster.  For example, an EBS volume
        identifier (``vol-4282672b``).  This is used to address the block
        device for operations like attach and detach.
    :ivar int size: The size, in bytes, of the block device.
    :ivar unicode host: The IP address of the host to which the block device is
        attached or ``None`` if it is currently unattached.
    :ivar UUID dataset_id: The Flocker dataset ID associated with this volume.
    """
    blockdevice_id = field(type=unicode, mandatory=True)
    size = field(type=int, mandatory=True)
    host = field(type=(unicode, type(None)), initial=None)
    dataset_id = field(type=UUID, mandatory=True)


# Replace this with a simpler factory-function based API like:
#
#     change = destroy_blockdevice_dataset(volme)
#
# after FLOC-1591 makes it possible to have reasonable logging with such a
# solution.
@_logged_statechange
@implementer(IStateChange)
class DestroyBlockDeviceDataset(PRecord):
    """
    Destroy the volume for a dataset with a primary manifestation on the node
    where this state change runs.

    :ivar UUID dataset_id: The unique identifier of the dataset to which the
        volume to be destroyed belongs.
    """
    dataset_id = field(type=UUID, mandatory=True)

    @property
    def _eliot_action(self):
        return DESTROY_BLOCK_DEVICE_DATASET(
            _logger, dataset_id=self.dataset_id
        )

    def run(self, deployer):
        for volume in deployer.block_device_api.list_volumes():
            if volume.dataset_id == self.dataset_id:
                return Sequentially(
                    changes=[
                        UnmountBlockDevice(volume=volume),
                        DetachVolume(volume=volume),
                        DestroyVolume(volume=volume),
                    ]
                ).run(deployer)
        return succeed(None)


def _volume():
    """
    Create and return a ``PRecord`` ``field`` to hold a ``BlockDeviceVolume``.
    """
    return field(
        type=BlockDeviceVolume, mandatory=True,
        # Disable the automatic PRecord.create factory.  Callers can just
        # supply the right type, we don't need the magic coercion behavior
        # supplied by default.
        factory=lambda x: x
    )


@_logged_statechange
@implementer(IStateChange)
class UnmountBlockDevice(PRecord):
    """
    Unmount the filesystem mounted from the block device backed by a particular
    volume.

    :ivar BlockDeviceVolume volume: The volume associated with the dataset
        which will be unmounted.
    """
    volume = _volume()

    @property
    def _eliot_action(self):
        return UNMOUNT_BLOCK_DEVICE(_logger, volume=self.volume)

    def run(self, deployer):
        """
        Run the system ``unmount`` tool to unmount this change's volume's block
        device.  The volume must be attached to this node and the corresponding
        block device mounted.
        """
        device = deployer.block_device_api.get_device_path(
            self.volume.blockdevice_id
        )
        # This should be asynchronous.  Do it as part of FLOC-1499.  Make sure
        # to fix _logged_statechange to handle Deferreds too.
        check_output([b"umount", device.path])
        return succeed(None)


@_logged_statechange
@implementer(IStateChange)
class DetachVolume(PRecord):
    """
    Detach a volume from the node it is currently attached to.

    :ivar BlockDeviceVolume volume: The volume to destroy.
    """
    volume = _volume()

    @property
    def _eliot_action(self):
        return DETACH_VOLUME(_logger, volume=self.volume)

    def run(self, deployer):
        """
        Use the deployer's ``IBlockDeviceAPI`` to detach the volume.
        """
        # Make this asynchronous after FLOC-1549, probably as part of
        # FLOC-1593.
        deployer.block_device_api.detach_volume(self.volume.blockdevice_id)
        return succeed(None)


@_logged_statechange
@implementer(IStateChange)
class DestroyVolume(PRecord):
    """
    Destroy the storage (and therefore contents) of a volume.

    :ivar BlockDeviceVolume volume: The volume to destroy.
    """
    volume = _volume()

    @property
    def _eliot_action(self):
        return DESTROY_VOLUME(_logger, volume=self.volume)

    def run(self, deployer):
        """
        Use the deployer's ``IBlockDeviceAPI`` to destroy the volume.
        """
        # Make this asynchronous as part of FLOC-1549.
        deployer.block_device_api.destroy_volume(self.volume.blockdevice_id)
        return succeed(None)


@implementer(IStateChange)
class CreateBlockDeviceDataset(PRecord):
    """
    An operation to create a new dataset on a newly created volume with a newly
    initialized filesystem.

    :ivar Dataset dataset: The dataset for which to create a block device.
    :ivar FilePath mountpoint: The path at which to mount the created device.
    """
    dataset = field(mandatory=True, type=Dataset)
    mountpoint = field(mandatory=True, type=FilePath)

    def run(self, deployer):
        """
        Create a block device, attach it to the local host, create an ``ext4``
        filesystem on the device and mount it.

        Operations are performed synchronously.

        See ``IStateChange.run`` for general argument and return type
        documentation.

        :returns: An already fired ``Deferred`` with result ``None``.
        """
        with CREATE_BLOCK_DEVICE_DATASET(
                _logger,
                dataset=self.dataset, mountpoint=self.mountpoint
        ) as action:
            api = deployer.block_device_api
            volume = api.create_volume(
                dataset_id=UUID(self.dataset.dataset_id),
                size=self.dataset.maximum_size,
            )

            # This will be factored into a separate IStateChange to support the
            # case where the volume exists but is not attached.  That object
            # will be used by this one to perform this work.  FLOC-1575
            volume = api.attach_volume(
                volume.blockdevice_id, deployer.hostname
            )
            device = api.get_device_path(volume.blockdevice_id)

            # This will be factored into a separate IStateChange to support the
            # case where the volume is attached but has no filesystem.  That
            # object will be used by this one to perform this work. FLOC-1576
            check_output(["mkfs", "-t", "ext4", device.path])

            # This will be factored into a separate IStateChange to support the
            # case where the only state change necessary is mounting.  That
            # object will be used by this one to perform this mount. It will
            # also gracefully handle the case where the mountpoint directory
            # already exists.  FLOC-1498
            self.mountpoint.makedirs()
            check_output(["mount", device.path, self.mountpoint.path])

            action.add_success_fields(
                block_device_path=device,
                block_device_id=volume.blockdevice_id,
                dataset_id=volume.dataset_id,
                block_device_size=volume.size,
                block_device_host=volume.host,
            )
        return succeed(None)


# TODO: Introduce a non-blocking version of this interface and an automatic
# thread-based wrapper for adapting this to the other.  Use that interface
# anywhere being non-blocking is important (which is probably lots of places).
# See https://clusterhq.atlassian.net/browse/FLOC-1549
class IBlockDeviceAPI(Interface):
    """
    Common operations provided by all block device backends.

    Note: This is an early sketch of the interface and it'll be refined as we
    real blockdevice providers are implemented.
    """
    def create_volume(dataset_id, size):
        """
        Create a new volume.

        XXX: Probably needs to be some checking of valid sizes for different
        backends. Perhaps the allowed sizes should be defined as constants?

        :param UUID dataset_id: The Flocker dataset ID of the dataset on this
            volume.
        :param int size: The size of the new volume in bytes.
        :returns: A ``BlockDeviceVolume``.
        """

    def destroy_volume(blockdevice_id):
        """
        Destroy an existing volume.

        :param unicode blockdevice_id: The unique identifier for the volume to
            destroy.

        :raises UnknownVolume: If the supplied ``blockdevice_id`` does not
            exist.

        :return: ``None``
        """

    def attach_volume(blockdevice_id, host):
        """
        Attach ``blockdevice_id`` to ``host``.

        :param unicode blockdevice_id: The unique identifier for the block
            device being attached.
        :param unicode host: The IP address of a host to attach the volume to.
        :raises UnknownVolume: If the supplied ``blockdevice_id`` does not
            exist.
        :raises AlreadyAttachedVolume: If the supplied ``blockdevice_id`` is
            already attached.
        :returns: A ``BlockDeviceVolume`` with a ``host`` attribute set to
            ``host``.
        """

    def detach_volume(blockdevice_id):
        """
        Detach ``blockdevice_id`` from whatever host it is attached to.

        :param unicode blockdevice_id: The unique identifier for the block
            device being detached.

        :raises UnknownVolume: If the supplied ``blockdevice_id`` does not
            exist.
        :raises UnattachedVolume: If the supplied ``blockdevice_id`` is
            not attached to anything.
        :returns: ``None``
        """

    def resize_volume(blockdevice_id, size):
        """
        Resize an unattached ``blockdevice_id``.

        This changes the amount of storage available.  It does not change the
        data on the volume (including the filesystem).

        :param unicode blockdevice_id: The unique identifier for the block
            device being detached.
        :param int size: The required size, in bytes, of the volume.

        :raises UnknownVolume: If the supplied ``blockdevice_id`` does not
            exist.

        :returns: ``None``
        """

    def list_volumes():
        """
        List all the block devices available via the back end API.

        :returns: A ``list`` of ``BlockDeviceVolume``s.
        """

    def get_device_path(blockdevice_id):
        """
{'fraction_non_alphanumeric': 0.07795750690031453, 'fraction_numerical': 0.003402015533731305, 'mean_word_length': 2.6473135900737446, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5266976', 'n_tokens_mistral': 8724, 'n_tokens_neox': 8404, 'n_words': 2990}