# -*- coding: utf-8 -*-
'''
States for managing zpools

:maintainer:    Jorge Schrauwen <sjorge@blackdot.be>
:maturity:      new
:depends:       salt.utils.zfs, salt.modules.zpool
:platform:      smartos, illumos, solaris, freebsd, linux

.. versionadded:: 2016.3.0
.. versionchanged:: 2018.3.1
  Big refactor to remove duplicate code, better type converions and improved
  consistancy in output.

.. code-block:: yaml

    oldpool:
      zpool.absent:
        - export: true

    newpool:
      zpool.present:
        - config:
            import: false
            force: true
        - properties:
            comment: salty storage pool
        - layout:
            - mirror:
              - /dev/disk0
              - /dev/disk1
            - mirror:
              - /dev/disk2
              - /dev/disk3

    partitionpool:
      zpool.present:
        - config:
            import: false
            force: true
        - properties:
            comment: disk partition salty storage pool
            ashift: '12'
            feature@lz4_compress: enabled
        - filesystem_properties:
            compression: lz4
            atime: on
            relatime: on
        - layout:
            - /dev/disk/by-uuid/3e43ce94-77af-4f52-a91b-6cdbb0b0f41b

    simplepool:
      zpool.present:
        - config:
            import: false
            force: true
        - properties:
            comment: another salty storage pool
        - layout:
            - /dev/disk0
            - /dev/disk1

.. warning::

    The layout will never be updated, it will only be used at time of creation.
    It's a whole lot of work to figure out if a devices needs to be detached, removed,
    etc. This is best done by the sysadmin on a case per case basis.

    Filesystem properties are also not updated, this should be managed by the zfs state module.

'''
from __future__ import absolute_import, print_function, unicode_literals

# Import Python libs
import os
import logging

# Import Salt libs
from salt.utils.odict import OrderedDict

log = logging.getLogger(__name__)

# Define the state's virtual name
__virtualname__ = 'zpool'


def __virtual__():
    '''
    Provides zpool state
    '''
    if __grains__['zfs_support']:
        return __virtualname__
    else:
        return False, 'The zpool state cannot be loaded: zfs not supported'


def _layout_to_vdev(layout, device_dir=None):
    '''
    Turn the layout data into usable vdevs spedcification

    We need to support 2 ways of passing the layout:

    .. code::
        layout_new:
          - mirror:
            - disk0
            - disk1
          - mirror:
            - disk2
            - disk3

    .. code:
        layout_legacy:
          mirror-0:
            disk0
            disk1
          mirror-1:
            disk2
            disk3

    '''
    vdevs = []

    # NOTE: check device_dir exists
    if device_dir and not os.path.exists(device_dir):
        device_dir = None

    # NOTE: handle list of OrderedDicts (new layout)
    if isinstance(layout, list):
        # NOTE: parse each vdev as a tiny layout and just append
        for vdev in layout:
            if isinstance(vdev, OrderedDict):
                vdevs.extend(_layout_to_vdev(vdev, device_dir))
            else:
                if device_dir and vdev[0] != '/':
                    vdev = os.path.join(device_dir, vdev)
                vdevs.append(vdev)

    # NOTE: handle nested OrderedDict (legacy layout)
    #       this is also used to parse the nested OrderedDicts
    #       from the new layout
    elif isinstance(layout, OrderedDict):
        for vdev in layout:
            # NOTE: extract the vdev type and disks in the vdev
            vdev_type = vdev.split('-')[0]
            vdev_disk = layout[vdev]

            # NOTE: skip appending the dummy type 'disk'
            if vdev_type != 'disk':
                vdevs.append(vdev_type)

            # NOTE: ensure the disks are a list (legacy layout are not)
            if not isinstance(vdev_disk, list):
                vdev_disk = vdev_disk.split(' ')

            # NOTE: also append the actualy disks behind the type
            #       also prepend device_dir to disks if required
            for disk in vdev_disk:
                if device_dir and disk[0] != '/':
                    disk = os.path.join(device_dir, disk)
                vdevs.append(disk)

    # NOTE: we got invalid data for layout
    else:
        vdevs = None

    return vdevs


def present(name, properties=None, filesystem_properties=None, layout=None, config=None):
    '''
    ensure storage pool is present on the system

    name : string
        name of storage pool
    properties : dict
        optional set of properties to set for the storage pool
    filesystem_properties : dict
        optional set of filesystem properties to set for the storage pool (creation only)
    layout: dict
        disk layout to use if the pool does not exist (creation only)
    config : dict
        fine grain control over this state

    .. note::

        The following configuration properties can be toggled in the config parameter.
          - import (true) - try to import the pool before creating it if absent
          - import_dirs (None) - specify additional locations to scan for devices on import (comma-seperated)
          - device_dir (None, SunOS=/dev/dsk, Linux=/dev) - specify device directory to prepend for none
            absolute device paths
          - force (false) - try to force the import or creation

    .. note::

        It is no longer needed to give a unique name to each top-level vdev, the old
        layout format is still supported but no longer recommended.

        .. code-block:: yaml

            - mirror:
              - /tmp/vdisk3
              - /tmp/vdisk2
            - mirror:
              - /tmp/vdisk0
              - /tmp/vdisk1

        The above yaml will always result in the following zpool create:

        .. code-block:: bash

            zpool create mypool mirror /tmp/vdisk3 /tmp/vdisk2 mirror /tmp/vdisk0 /tmp/vdisk1

    .. warning::

        The legacy format is also still supported but not recommended,
        because ID's inside the layout dict must be unique they need to have a suffix.

        .. code-block:: yaml

            mirror-0:
              /tmp/vdisk3
              /tmp/vdisk2
            mirror-1:
              /tmp/vdisk0
              /tmp/vdisk1

    .. warning::

        Pay attention to the order of your dict!

        .. code-block:: yaml

            - mirror:
              - /tmp/vdisk0
              - /tmp/vdisk1
            - /tmp/vdisk2

        The above will result in the following zpool create:

        .. code-block:: bash

            zpool create mypool mirror /tmp/vdisk0 /tmp/vdisk1 /tmp/vdisk2

        Creating a 3-way mirror! While you probably expect it to be mirror
        root vdev with 2 devices + a root vdev of 1 device!

    '''
    ret = {'name': name,
           'changes': {},
           'result': None,
           'comment': ''}

    # config defaults
    default_config = {
        'import': True,
        'import_dirs': None,
        'device_dir': None,
        'force': False
    }
    if __grains__['kernel'] == 'SunOS':
        default_config['device_dir'] = '/dev/dsk'
    elif __grains__['kernel'] == 'Linux':
        default_config['device_dir'] = '/dev'

    # merge state config
    if config:
        default_config.update(config)
    config = default_config

    # ensure properties are zfs values
    if properties:
        properties = __utils__['zfs.from_auto_dict'](properties)
    elif properties is None:
        properties = {}
    if filesystem_properties:
        filesystem_properties = __utils__['zfs.from_auto_dict'](filesystem_properties)
    elif filesystem_properties is None:
        filesystem_properties = {}

    # parse layout
    vdevs = _layout_to_vdev(layout, config['device_dir'])
    if vdevs:
        vdevs.insert(0, name)

    # log configuration
    log.debug('zpool.present::%s::config - %s', name, config)
    log.debug('zpool.present::%s::vdevs - %s', name, vdevs)
    log.debug('zpool.present::%s::properties -  %s', name, properties)
    log.debug('zpool.present::%s::filesystem_properties -  %s', name, filesystem_properties)

    # ensure the pool is present
    ret['result'] = False

    # don't do anything because this is a test
    if __opts__['test']:
        ret['result'] = True
        if __salt__['zpool.exists'](name):
            ret['changes'][name] = 'uptodate'
        else:
            ret['changes'][name] = 'imported' if config['import'] else 'created'
        ret['comment'] = 'storage pool {0} was {1}'.format(name, ret['changes'][name])

    # update pool
    elif __salt__['zpool.exists'](name):
        ret['result'] = True

        # fetch current pool properties
        properties_current = __salt__['zpool.get'](name, parsable=True)

        # build list of properties to update
        properties_update = []
        if properties:
            for prop in properties:
                # skip unexisting properties
                if prop not in properties_current:
                    log.warning('zpool.present::%s::update - unknown property: %s', name, prop)
                    continue

                # compare current and wanted value
                if properties_current[prop] != properties[prop]:
                    properties_update.append(prop)

        # update pool properties
        for prop in properties_update:
            res = __salt__['zpool.set'](name, prop, properties[prop])

            if res['set']:
                if name not in ret['changes']:
                    ret['changes'][name] = {}
                ret['changes'][name][prop] = properties[prop]
            else:
                ret['result'] = False
                if ret['comment'] == '':
                    ret['comment'] = 'The following properties were not updated:'
                ret['comment'] = '{0} {1}'.format(ret['comment'], prop)

        if ret['result']:
            ret['comment'] = 'properties updated' if ret['changes'] else 'no update needed'

    # import or create the pool (at least try to anyway)
    else:
        # import pool
        if config['import']:
            mod_res = __salt__['zpool.import'](
                name,
                force=config['force'],
                dir=config['import_dirs'],
            )

            ret['result'] = mod_res['imported']
            if ret['result']:
                ret['changes'][name] = 'imported'
                ret['comment'] = 'storage pool {0} was imported'.format(name)

        # create pool
        if not ret['result'] and vdevs:
            log.debug('zpool.present::%s::creating', name)

            # execute zpool.create
            mod_res = __salt__['zpool.create'](
                *vdevs,
                force=config['force'],
                properties=properties,
                filesystem_properties=filesystem_properties
            )

            ret['result'] = mod_res['created']
            if ret['result']:
                ret['changes'][name] = 'created'
                ret['comment'] = 'storage pool {0} was created'.format(name)
            elif 'error' in mod_res:
                ret['comment'] = mod_res['error']
            else:
                ret['comment'] = 'could not create storage pool {0}'.format(name)

        # give up, we cannot import the pool and we do not have a layout to create it
        if not ret['result'] and not vdevs:
            ret['comment'] = 'storage pool {0} was not imported, no (valid) layout specified for creation'.format(name)

    return ret


def absent(name, export=False, force=False):
    '''
    ensure storage pool is absent on the system

    name : string
        name of storage pool
    export : boolean
        export instread of destroy the zpool if present
    force : boolean
        force destroy or export

    '''
    ret = {'name': name,
           'changes': {},
           'result': None,
           'comment': ''}

    # log configuration
    log.debug('zpool.absent::%s::config::force = %s', name, force)
    log.debug('zpool.absent::%s::config::export = %s', name, export)

    # ensure the pool is absent
    if __salt__['zpool.exists'](name):  # looks like we need to do some work
        mod_res = {}
        ret['result'] = False

        # NOTE: handle test
        if __opts__['test']:
            ret['result'] = True

        # NOTE: try to export the pool
        elif export:
            mod_res = __salt__['zpool.export'](name, force=force)
            ret['result'] = mod_res['exported']

        # NOTE: try to destroy the pool
        else:
            mod_res = __salt__['zpool.destroy'](name, force=force)
            ret['result'] = mod_res['destroyed']

        if ret['result']:  # update the changes and comment
            ret['changes'][name] = 'exported' if export else 'destroyed'
            ret['comment'] = 'storage pool {0} was {1}'.format(name, ret['changes'][name])
        elif 'error' in mod_res:
            ret['comment'] = mod_res['error']

    else:  # we are looking good
        ret['result'] = True
        ret['comment'] = 'storage pool {0} is absent'.format(name)

    return ret

# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
