# Copyright 2018,2021 IBM Corp. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
A :class:`~zhmcclient.StorageVolume` object represents an FCP or FICON (ECKD)
:term:`storage volume` that is defined in a :term:`storage group`.

Storage volume objects can be created, but what is created is the definition
of a storage volume in the HMC and CPC. This does not include the act of
actually allocating the volume on a storage subsystem. That is performed by a
storage administrator who :term:`fulfills <fulfillment>` the volumes.

In order to represent that, storage volume objects have a fulfillment state
that is available in their 'fulfillment-state' property.

When a storage group is attached to a :term:`partition`, the group's
storage volumes are attached to the partition and any :term:`virtual storage
resource` objects are instantiated.

Storage volumes are contained in :term:`storage groups <storage group>`.

You can create as many storage volumes as you want in a storage group.

Storage groups and storage volumes only can be defined in CPCs that are in
DPM mode and that have the "dpm-storage-management"
:ref:`firmware feature <firmware features>` enabled.
"""


import re
import copy
# from requests.utils import quote

from ._manager import BaseManager
from ._resource import BaseResource
from ._logging import logged_api_call
from ._utils import RC_STORAGE_VOLUME

__all__ = ['StorageVolumeManager', 'StorageVolume']


class StorageVolumeManager(BaseManager):
    """
    Manager providing access to the :term:`storage volumes <storage volume>`
    in a particular :term:`storage group`.

    Derived from :class:`~zhmcclient.BaseManager`; see there for common methods
    and attributes.

    Objects of this class are not directly created by the user; they are
    accessible via the following instance variable of a
    :class:`~zhmcclient.StorageGroup` object:

    * :attr:`~zhmcclient.StorageGroup.storage_volumes`

    HMC/SE version requirements:

    * :ref:`firmware feature <firmware features>` "dpm-storage-management"
    """

    def __init__(self, storage_group):
        # This function should not go into the docs.
        # Parameters:
        #   storage_group (:class:`~zhmcclient.StorageGroup`):
        #     Storage group defining the scope for this manager.

        # Resource properties that are supported as filter query parameters.
        # If the support for a resource property changes within the set of HMC
        # versions that support this type of resource, this list must be set up
        # for the version of the HMC this session is connected to.
        query_props = [
            'name',
            'fulfillment-state',
            'maximum-size',
            'minimum-size',
            'usage',
        ]

        super().__init__(
            resource_class=StorageVolume,
            class_name=RC_STORAGE_VOLUME,
            session=storage_group.manager.session,
            parent=storage_group,
            base_uri=f'{storage_group.uri}/storage-volumes',
            oid_prop='element-id',
            uri_prop='element-uri',
            name_prop='name',
            query_props=query_props)

    @property
    def storage_group(self):
        """
        :class:`~zhmcclient.StorageGroup`: :term:`Storage group` defining the
        scope for this manager.
        """
        return self._parent

    @logged_api_call
    def list(self, full_properties=False, filter_args=None):
        """
        List the storage volumes in this storage group.

        Any resource property may be specified in a filter argument. For
        details about filter arguments, see :ref:`Filtering`.

        The listing of resources is handled in an optimized way:

        * If this manager is enabled for :ref:`auto-updating`, a locally
          maintained resource list is used (which is automatically updated via
          inventory notifications from the HMC) and the provided filter
          arguments are applied.

        * Otherwise, if the filter arguments specify the resource name as a
          single filter argument with a straight match string (i.e. without
          regular expressions), an optimized lookup is performed based on a
          locally maintained name-URI cache.

        * Otherwise, the HMC List operation is performed with the subset of the
          provided filter arguments that can be handled on the HMC side and the
          remaining filter arguments are applied on the client side on the list
          result.

        HMC/SE version requirements:

        * :ref:`firmware feature <firmware features>` "dpm-storage-management"

        Authorization requirements:

        * Object-access permission to this storage group.

        Parameters:

          full_properties (bool):
            Controls that the full set of resource properties for each returned
            storage volume is being retrieved, vs. only the following short
            set: "element-uri", "name", "fulfillment-state", "size", and
            "usage".

          filter_args (dict):
            Filter arguments that narrow the list of returned resources to
            those that match the specified filter arguments. For details, see
            :ref:`Filtering`.

            `None` causes no filtering to happen, i.e. all resources are
            returned.

        Returns:

          : A list of :class:`~zhmcclient.StorageVolume` objects.

        Raises:

          :exc:`~zhmcclient.HTTPError`
          :exc:`~zhmcclient.ParseError`
          :exc:`~zhmcclient.AuthError`
          :exc:`~zhmcclient.ConnectionError`
        """
        result_prop = 'storage-volumes'
        list_uri = f'{self.storage_group.uri}/storage-volumes'
        return self._list_with_operation(
            list_uri, result_prop, full_properties, filter_args, None)

    @logged_api_call
    def create(self, properties, email_to_addresses=None,
               email_cc_addresses=None, email_insert=None):
        """
        Create a :term:`storage volume` in this storage group on the HMC, and
        optionally send emails to storage administrators requesting creation of
        the storage volume on the storage subsystem and setup of any resources
        related to the storage volume (e.g. LUN mask definition on the storage
        subsystem).

        This method performs the "Modify Storage Group Properties" operation,
        requesting creation of the volume.

        HMC/SE version requirements:

        * :ref:`firmware feature <firmware features>` "dpm-storage-management"

        Authorization requirements:

        * Object-access permission to this storage group.
        * Task permission to the "Configure Storage - System Programmer" task.

        Parameters:

          properties (dict): Initial property values for the new volume.

            Allowable properties are the fields defined in the
            "storage-volume-request-info" nested object described for
            operation "Modify Storage Group Properties" in the
            :term:`HMC API` book.
            The valid fields are those for the "create" operation. The
            `operation` field must not be provided; it is set automatically
            to the value "create".

            The properties provided in this parameter will be copied and then
            amended with the `operation="create"` field, and then used as a
            single array item for the `storage-volumes` field in the request
            body of the "Modify Storage Group Properties" operation.

            Note that for storage volumes, the HMC does auto-generate a value
            for the "name" property, but that auto-generated name is not unique
            within the parent storage group. If you depend on a unique name,
            you need to specify a "name" property accordingly.

          email_to_addresses (:term:`iterable` of :term:`string`): Email
            addresses of one or more storage administrator to be notified.
            If `None` or empty, no email will be sent.

          email_cc_addresses (:term:`iterable` of :term:`string`): Email
            addresses of one or more storage administrator to be copied
            on the notification email.
            If `None` or empty, nobody will be copied on the email.
            Must be `None` or empty if `email_to_addresses` is `None` or empty.

          email_insert (:term:`string`): Additional text to be inserted in the
            notification email.
            The text can include HTML formatting tags.
            If `None`, no additional text will be inserted.
            Must be `None` or empty if `email_to_addresses` is `None` or empty.

        Returns:

          StorageVolume:
            The resource object for the new storage volume.
            The object will have the following properties set:

            - 'element-uri' as returned by the HMC
            - 'element-id' as determined from the 'element-uri' property
            - 'class' and 'parent'
            - additional properties as specified in the input properties

        Raises:

          :exc:`~zhmcclient.HTTPError`
          :exc:`~zhmcclient.ParseError`
          :exc:`~zhmcclient.AuthError`
          :exc:`~zhmcclient.ConnectionError`

        Example::

            stovol1 = fcp_stogrp.storage_volumes.create(
                properties=dict(
                    name='vol1',
                    size=30,  # GiB
            ))
        """

        volreq_obj = copy.deepcopy(properties)
        volreq_obj['operation'] = 'create'
        body = {
            'storage-volumes': [volreq_obj],
        }
        if email_to_addresses:
            body['email-to-addresses'] = email_to_addresses
            if email_cc_addresses:
                body['email-cc-addresses'] = email_cc_addresses
            if email_insert:
                body['email-insert'] = email_insert
        else:
            if email_cc_addresses:
                raise ValueError(
                    "email_cc_addresses must not be specified if there is no "
                    f"email_to_addresses: {email_cc_addresses}")
            if email_insert:
                raise ValueError(
                    "email_insert must not be specified if there is no "
                    f"email_to_addresses: {email_insert}")

        result = self.session.post(
            self.storage_group.uri + '/operations/modify',
            body=body)
        uri = result['element-uris'][0]

        storage_volume = self.resource_object(uri, properties)

        # The name is not guaranteed to be unique, so we don't maintain
        # a name-to-uri cache for storage volumes.

        return storage_volume


class StorageVolume(BaseResource):
    """
    Representation of a :term:`storage volume`.

    Derived from :class:`~zhmcclient.BaseResource`; see there for common
    methods and attributes.

    Objects of this class are not directly created by the user; they are
    returned from creation or list functions on their manager object
    (in this case, :class:`~zhmcclient.StorageVolumeManager`).

    HMC/SE version requirements:

    * :ref:`firmware feature <firmware features>` "dpm-storage-management"
    """

    def __init__(self, manager, uri, name=None, properties=None):
        # This function should not go into the docs.
        #   manager (:class:`~zhmcclient.StorageVolumeManager`):
        #     Manager object for this resource object.
        #   uri (string):
        #     Canonical URI path of the resource.
        #   name (string):
        #     Name of the resource.
        #   properties (dict):
        #     Properties to be set for this resource object. May be `None` or
        #     empty.
        assert isinstance(manager, StorageVolumeManager), (
            "StorageVolume init: Expected manager type "
            f"{StorageVolumeManager}, got {type(manager)}")
        super().__init__(manager, uri, name, properties)

    @property
    def oid(self):
        """
        :term `unicode string`: The object ID of this storage volume.
        The object ID is unique within the parent storage group.

        Note that for storage volumes, the 'name' property is not unique and
        therefore cannot be used to identify a storage volume. Therefore,
        storage volumes provide easy access to the object ID, as a means to
        identify the storage volume in CLIs and other string-based tooling.
        """
        m = re.match(r'^/api/storage-groups/[^/]*/storage-volumes/([^/]*)$',
                     self.uri)
        oid = m.group(1)
        return oid

    @logged_api_call
    def delete(self, email_to_addresses=None, email_cc_addresses=None,
               email_insert=None):
        """
        Delete this storage volume on the HMC, and optionally send emails to
        storage administrators requesting deletion of the storage volume on the
        storage subsystem and cleanup of any resources related to the storage
        volume (e.g. LUN mask definitions on a storage subsystem).

        This method performs the "Modify Storage Group Properties" operation,
        requesting deletion of the volume.

        HMC/SE version requirements:

        * :ref:`firmware feature <firmware features>` "dpm-storage-management"

        Authorization requirements:

        * Object-access permission to the storage group owning this storage
          volume.
        * Task permission to the "Configure Storage - System Programmer" task.

        Parameters:

          email_to_addresses (:term:`iterable` of :term:`string`): Email
            addresses of one or more storage administrator to be notified.
            If `None` or empty, no email will be sent.

          email_cc_addresses (:term:`iterable` of :term:`string`): Email
            addresses of one or more storage administrator to be copied
            on the notification email.
            If `None` or empty, nobody will be copied on the email.
            Must be `None` or empty if `email_to_addresses` is `None` or empty.

          email_insert (:term:`string`): Additional text to be inserted in the
            notification email.
            The text can include HTML formatting tags.
            If `None`, no additional text will be inserted.
            Must be `None` or empty if `email_to_addresses` is `None` or empty.

        Raises:

          :exc:`~zhmcclient.HTTPError`
          :exc:`~zhmcclient.ParseError`
          :exc:`~zhmcclient.AuthError`
          :exc:`~zhmcclient.ConnectionError`
        """

        volreq_obj = {
            'operation': 'delete',
            'element-uri': self.uri,
        }
        body = {
            'storage-volumes': [
                volreq_obj
            ],
        }
        if email_to_addresses:
            body['email-to-addresses'] = email_to_addresses
            if email_cc_addresses:
                body['email-cc-addresses'] = email_cc_addresses
            if email_insert:
                body['email-insert'] = email_insert
        else:
            if email_cc_addresses:
                raise ValueError(
                    "email_cc_addresses must not be specified if there is no "
                    f"email_to_addresses: {email_cc_addresses!r}")
            if email_insert:
                raise ValueError(
                    "email_insert must not be specified if there is no "
                    f"email_to_addresses: {email_insert}")

        self.manager.session.post(
            self.manager.storage_group.uri + '/operations/modify',
            resource=self, body=body)

        # pylint: disable=protected-access
        self.manager._name_uri_cache.delete(
            self.get_properties_local(self.manager._name_prop, None))

        parent_sv_uris = self.manager.parent.get_properties_local(
            'storage-volume-uris')
        if parent_sv_uris:
            try:
                parent_sv_uris.remove(self._uri)
            except ValueError:
                pass

        self.cease_existence_local()

    @logged_api_call
    def update_properties(self, properties, email_to_addresses=None,
                          email_cc_addresses=None, email_insert=None):
        """
        Update writeable properties of this storage volume on the HMC, and
        optionally send emails to storage administrators requesting
        modification of the storage volume on the storage subsystem and of any
        resources related to the storage volume.

        This method performs the "Modify Storage Group Properties" operation,
        requesting modification of the volume.

        This method serializes with other methods that access or change
        properties on the same Python object.

        HMC/SE version requirements:

        * :ref:`firmware feature <firmware features>` "dpm-storage-management"

        Authorization requirements:

        * Object-access permission to the storage group owning this storage
          volume.
        * Task permission to the "Configure Storage - System Programmer" task.

        Parameters:

          properties (dict): New property values for the volume.
            Allowable properties are the fields defined in the
            "storage-volume-request-info" nested object for the "modify"
            operation. That nested object is described in section "Request body
            contents" for operation "Modify Storage Group Properties" in the
            :term:`HMC API` book.

            The properties provided in this parameter will be copied and then
            amended with the `operation="modify"` and `element-uri` properties,
            and then used as a single array item for the `storage-volumes`
            field in the request body of the "Modify Storage Group Properties"
            operation.

          email_to_addresses (:term:`iterable` of :term:`string`): Email
            addresses of one or more storage administrator to be notified.
            If `None` or empty, no email will be sent.

          email_cc_addresses (:term:`iterable` of :term:`string`): Email
            addresses of one or more storage administrator to be copied
            on the notification email.
            If `None` or empty, nobody will be copied on the email.
            Must be `None` or empty if `email_to_addresses` is `None` or empty.

          email_insert (:term:`string`): Additional text to be inserted in the
            notification email.
            The text can include HTML formatting tags.
            If `None`, no additional text will be inserted.
            Must be `None` or empty if `email_to_addresses` is `None` or empty.

        Raises:

          :exc:`~zhmcclient.HTTPError`
          :exc:`~zhmcclient.ParseError`
          :exc:`~zhmcclient.AuthError`
          :exc:`~zhmcclient.ConnectionError`
        """

        volreq_obj = copy.deepcopy(properties)
        volreq_obj['operation'] = 'modify'
        volreq_obj['element-uri'] = self.uri
        body = {
            'storage-volumes': [volreq_obj],
        }
        if email_to_addresses:
            body['email-to-addresses'] = email_to_addresses
            if email_cc_addresses:
                body['email-cc-addresses'] = email_cc_addresses
            if email_insert:
                body['email-insert'] = email_insert
        else:
            if email_cc_addresses:
                raise ValueError(
                    "email_cc_addresses must not be specified if there is no "
                    f"email_to_addresses: {email_cc_addresses!r}")
            if email_insert:
                raise ValueError(
                    "email_insert must not be specified if there is no "
                    f"email_to_addresses: {email_insert!r}")

        self.manager.session.post(
            self.manager.storage_group.uri + '/operations/modify',
            resource=self, body=body)

        self.update_properties_local(copy.deepcopy(properties))

    @logged_api_call
    def indicate_fulfillment_ficon(self, control_unit, unit_address):
        """
        TODO: Add ControlUnit objects etc for FICON support.

        Indicate completion of :term:`fulfillment` for this ECKD (=FICON)
        storage volume and provide identifying information (control unit
        and unit address) about the actual storage volume on the storage
        subsystem.

        Manually indicating fulfillment is required for all ECKD volumes,
        because they are not auto-discovered by the CPC.

        This method performs the "Fulfill FICON Storage Volume" HMC operation.

        Upon successful completion of this operation, the "fulfillment-state"
        property of this storage volume object will have been set to
        "complete". That is necessary for the CPC to be able to address and
        connect to the volume.

        If the "fulfillment-state" properties of all storage volumes in the
        owning storage group are "complete", the owning storage group's
        "fulfillment-state" property will also be set to "complete".

        HMC/SE version requirements:

        * :ref:`firmware feature <firmware features>` "dpm-storage-management"

        Authorization requirements:

        * Object-access permission to the storage group owning this storage
          volume.
        * Task permission to the "Configure Storage - Storage Administrator"
          task.

        Parameters:

          control_unit (:class:`~zhmcclient.ControlUnit`):
            Logical control unit (LCU) in which the backing ECKD volume is
            defined.

          unit_address (:term:`string`):
            Unit address of the backing ECKD volume within its logical control
            unit,
            as a hexadecimal number of up to 2 characters in any lexical case.

        Raises:

          :exc:`~zhmcclient.HTTPError`
          :exc:`~zhmcclient.ParseError`
          :exc:`~zhmcclient.AuthError`
          :exc:`~zhmcclient.ConnectionError`
        """

        # The operation requires exactly 2 characters in lower case
        unit_address_2 = format(int(unit_address, 16), '02x')

        body = {
            'control-unit-uri': control_unit.uri,
            'unit-address': unit_address_2,
        }
        self.manager.session.post(
            self.uri + '/operations/fulfill-ficon-storage-volume',
            resource=self, body=body)

    @logged_api_call
    def indicate_fulfillment_fcp(self, wwpn, lun, host_port):
        """
        Indicate completion of :term:`fulfillment` for this FCP storage volume
        and provide identifying information (WWPN and LUN) about the actual
        storage volume on the storage subsystem.

        Manually indicating fulfillment is required for storage volumes that
        will be used as boot devices for a partition. The specified host
        port will be used to access the storage volume during boot of the
        partition.

        Because the CPC discovers storage volumes automatically, the
        fulfillment of non-boot volumes does not need to be manually indicated
        using this function (it may be indicated though before the CPC
        discovers a working communications path to the volume, but the role
        of the specified host port is not clear in this case).

        This method performs the "Fulfill FCP Storage Volume" HMC operation.

        Upon successful completion of this operation, the "fulfillment-state"
        property of this storage volume object will have been set to
        "complete". That is necessary for the CPC to be able to address and
        connect to the volume.

        If the "fulfillment-state" properties of all storage volumes in the
        owning storage group are "complete", the owning storage group's
        "fulfillment-state" property will also be set to "complete".

        HMC/SE version requirements:

        * :ref:`firmware feature <firmware features>` "dpm-storage-management"

        Authorization requirements:

        * Object-access permission to the storage group owning this storage
          volume.
        * Task permission to the "Configure Storage - Storage Administrator"
          task.

        Parameters:

          wwpn (:term:`string`):
            World wide port name (WWPN) of the FCP storage subsystem containing
            the storage volume,
            as a hexadecimal number of up to 16 characters in any lexical case.

          lun (:term:`string`):
            Logical Unit Number (LUN) of the storage volume within its FCP
            storage subsystem,
            as a hexadecimal number of up to 16 characters in any lexical case.

          host_port (:class:`~zhmcclient.Port`):
            Storage port on the CPC that will be used to boot from.

        Raises:

          :exc:`~zhmcclient.HTTPError`
          :exc:`~zhmcclient.ParseError`
          :exc:`~zhmcclient.AuthError`
          :exc:`~zhmcclient.ConnectionError`
        """

        # The operation requires exactly 16 characters in lower case
        wwpn_16 = format(int(wwpn, 16), '016x')
        lun_16 = format(int(lun, 16), '016x')

        body = {
            'world-wide-port-name': wwpn_16,
            'logical-unit-number': lun_16,
            'adapter-port-uri': host_port.uri,
        }
        self.manager.session.post(
            self.uri + '/operations/fulfill-fcp-storage-volume',
            resource=self, body=body)
