"""
Management of Solaris Zones

:maintainer:    Jorge Schrauwen <sjorge@blackdot.be>
:maturity:      new
:depends:       salt.modules.zoneadm, salt.modules.zonecfg
:platform:      solaris

.. versionadded:: 2017.7.0

Below are some examples of how to use this state.
Lets start with creating a zone and installing it.

.. code-block:: yaml

    omipkg1_configuration:
      zone.present:
        - name: omipkg1
        - brand: ipkg
        - zonepath: /zones/omipkg1
        - properties:
          - autoboot: true
          - ip-type: exclusive
          - cpu-shares: 50
        - resources:
          - attr:
            - name: owner
            - value: Jorge Schrauwen
            - type: string
          - attr:
            - name: description
            - value: OmniOS ipkg zone for testing
            - type: string
          - capped-memory:
            - physical: 64M
    omipkg1_installation:
      zone.installed:
        - name: omipkg1
        - require:
            - zone: omipkg1_configuration
    omipkg1_running:
      zone.booted:
        - name: omipkg1
        - require:
            - zone: omipkg1_installation

A zone without network access is not very useful. We could update
the zone.present state in the example above to add a network interface
or we could use a separate state for this.

.. code-block:: yaml

    omipkg1_network:
      zone.resource_present:
        - name: omipkg1
        - resource_type: net
        - resource_selector_property: mac-addr
        - resource_selector_value: "02:08:20:a2:a3:10"
        - physical: znic1
        - require:
            - zone: omipkg1_configuration

Since this is a single tenant system having the owner attribute is pointless.
Let's remove that attribute.

.. note::
    The following state run the omipkg1_configuration state will add it again!
    If the entire configuration is managed it would be better to add resource_prune
    and optionally the resource_selector_property properties to the resource.

.. code-block:: yaml

    omipkg1_strip_owner:
      zone.resource_present:
        - name: omipkg1
        - resource_type: attr
        - resource_selector_property: name
        - resource_selector_value: owner
        - require:
            - zone: omipkg1_configuration

Let's bump the zone's CPU shares a bit.

.. note::
    The following state run the omipkg1_configuration state will set it to 50 again.
    Update the entire zone configuration is managed you should update it there instead.

.. code-block:: yaml

    omipkg1_more_cpu:
      zone.property_present:
        - name: omipkg1
        - property: cpu-shares
        - value: 100

Or we can remove the limit altogether!

.. note::
    The following state run the omipkg1_configuration state will set it to 50 again.
    Update the entire zone configuration is managed you should set the
    property to None (nothing after the :) instead.

.. code-block:: yaml

    omipkg1_no_cpu:
      zone.property_absent:
        - name: omipkg1
        - property: cpu-shares

"""

import logging

import salt.utils.args
import salt.utils.atomicfile
import salt.utils.files
from salt.exceptions import CommandExecutionError
from salt.modules.zonecfg import _parse_value, _zonecfg_resource_default_selectors
from salt.utils.dictupdate import merge as merge_dict
from salt.utils.odict import OrderedDict

log = logging.getLogger(__name__)

__func_alias__ = {
    "import_": "import",
}

# Define the state's virtual name
__virtualname__ = "zone"


def __virtual__():
    """
    Provides zone state on Solaris
    """
    if "zonecfg.create" in __salt__ and "zoneadm.install" in __salt__:
        return True
    else:
        return (
            False,
            "{} state module can only be loaded on Solaris platforms".format(
                __virtualname__
            ),
        )


def property_present(name, property, value):
    """
    Ensure property has a certain value

    name : string
        name of the zone
    property : string
        name of property
    value : string
        value of property

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

    ## sanitize input
    value = _parse_value(value)

    zones = __salt__["zoneadm.list"](installed=True, configured=True)
    if name in zones:
        ## zone exists
        zonecfg = __salt__["zonecfg.info"](name, show_all=True)
        if property not in zonecfg or zonecfg[property] != _parse_value(value):
            if __opts__["test"]:
                ret["result"] = True
            else:
                # update property
                zonecfg_res = __salt__["zonecfg.set_property"](name, property, value)
                ret["result"] = zonecfg_res["status"]
                if "messages" in zonecfg_res:
                    ret["comment"] = zonecfg_res["message"]
            if ret["result"]:
                ret["changes"][property] = _parse_value(value)
                if ret["comment"] == "":
                    ret["comment"] = "The property {} is was updated to {}.".format(
                        property, value
                    )
            elif ret["comment"] == "":
                if ret["comment"] == "":
                    ret["comment"] = "The property {} is was not updated to {}!".format(
                        property, value
                    )
        else:
            ret["result"] = True
            ret["comment"] = "The property {} is already set to {}.".format(
                property, value
            )
    else:
        ## zone does not exist
        ret["result"] = False
        ret["comment"] = (
            "The zone {} is not in the configured, installed, or booted state.".format(
                name
            )
        )

    return ret


def property_absent(name, property):
    """
    Ensure property is absent

    name : string
        name of the zone
    property : string
        name of property

    .. note::
        This does a zoneacfg clear call. So the property may be reset to a default value!
        Does has the side effect of always having to be called.

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

    zones = __salt__["zoneadm.list"](installed=True, configured=True)
    if name in zones:
        ## zone exists
        zonecfg = __salt__["zonecfg.info"](name, show_all=True)
        if property in zonecfg:
            if __opts__["test"]:
                ret["result"] = True
            else:
                # clear property
                zonecfg_res = __salt__["zonecfg.clear_property"](name, property)
                zonecfg_new = __salt__["zonecfg.info"](name, show_all=True)
                ret["result"] = zonecfg_res["status"]
                if "messages" in zonecfg_res:
                    ret["comment"] = zonecfg_res["message"]
            if ret["result"]:
                if property not in zonecfg_new:
                    ret["changes"][property] = None
                elif zonecfg[property] != zonecfg_new[property]:
                    ret["changes"][property] = zonecfg_new[property]
                if ret["comment"] == "":
                    ret["comment"] = f"The property {property} was cleared!"
            elif ret["comment"] == "":
                if ret["comment"] == "":
                    ret["comment"] = "The property {} did not get cleared!".format(
                        property
                    )
        else:
            ret["result"] = True
            ret["comment"] = f"The property {property} does not exist!"
    else:
        ## zone does not exist
        ret["result"] = False
        ret["comment"] = (
            "The zone {} is not in the configured, installed, or booted state.".format(
                name
            )
        )

    return ret


def resource_present(
    name, resource_type, resource_selector_property, resource_selector_value, **kwargs
):
    """
    Ensure resource exists with provided properties

    name : string
        name of the zone
    resource_type : string
        type of resource
    resource_selector_property : string
        unique resource identifier
    resource_selector_value : string
        value for resource selection
    kwargs : string|int|...
        resource properties

    .. warning::
        Both resource_selector_property and resource_selector_value must be
        provided, some properties like ``name`` are already reserved by salt in
        states.

    .. note::
        You can set both resource_selector_property and resource_selector_value
        to None for resources that do not require them.

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

    # sanitize input
    kwargs = salt.utils.args.clean_kwargs(**kwargs)
    resource_selector_value = _parse_value(resource_selector_value)
    for k, v in kwargs.items():
        kwargs[k] = _parse_value(kwargs[k])

    zones = __salt__["zoneadm.list"](installed=True, configured=True)
    if name in zones:
        ## zone exists
        zonecfg = __salt__["zonecfg.info"](name, show_all=True)

        ## update kwargs
        zonecfg_kwargs = {}
        zonecfg_kwargs.update(kwargs)
        zonecfg_kwargs["zone"] = name
        zonecfg_kwargs["resource_type"] = resource_type
        zonecfg_kwargs["resource_selector"] = resource_selector_property
        if resource_selector_property:
            zonecfg_kwargs[resource_selector_property] = resource_selector_value

        ## check update or add
        if resource_type in zonecfg:
            for resource in zonecfg[resource_type]:
                if (
                    not resource_selector_property
                    or resource[resource_selector_property] == resource_selector_value
                ):
                    ret["result"] = True
                    if resource_selector_property:
                        ret["comment"] = "the {} resource {} is up to date.".format(
                            resource_type,
                            resource_selector_value,
                        )
                    else:
                        ret["comment"] = "the {} resource is up to date.".format(
                            resource_type,
                        )

                    ## check if update reauired
                    for key in kwargs:
                        log.debug(
                            "zone.resource_preent - key=%s value=%s current_value=%s",
                            key,
                            resource[key] if key in resource else None,
                            _parse_value(kwargs[key]),
                        )
                        # note: something odd with ncpus property, we fix it here for now
                        if key == "ncpus" and key in kwargs:
                            kwargs[key] = f"{float(kwargs[key]):.2f}"

                        if key not in resource:
                            ret["result"] = None
                        elif resource[key] != _parse_value(kwargs[key]):
                            ret["result"] = None

                    ## do update
                    if ret["result"] is None:
                        if __opts__["test"]:
                            ret["result"] = True
                        else:
                            ## update resource
                            zonecfg_res = __salt__["zonecfg.update_resource"](
                                **zonecfg_kwargs
                            )
                            ret["result"] = zonecfg_res["status"]
                            if "message" in zonecfg_res:
                                ret["comment"] = zonecfg_res["message"]

                        if ret["result"]:
                            ret["changes"][resource_type] = {}
                            if resource_selector_property:
                                ret["changes"][resource_type][
                                    resource_selector_value
                                ] = {}
                            for key in kwargs if ret["result"] else []:
                                if resource_selector_property:
                                    ret["changes"][resource_type][
                                        resource_selector_value
                                    ][key] = _parse_value(kwargs[key])
                                else:
                                    ret["changes"][resource_type][key] = _parse_value(
                                        kwargs[key]
                                    )
                            if ret["comment"] == "":
                                if resource_selector_property:
                                    ret["comment"] = (
                                        "The {} resource {} was updated.".format(
                                            resource_type,
                                            resource_selector_value,
                                        )
                                    )
                                else:
                                    ret["comment"] = (
                                        "The {} resource was updated.".format(
                                            resource_type,
                                        )
                                    )
                        elif ret["comment"] == "":
                            if resource_selector_property:
                                ret["comment"] = (
                                    "The {} resource {} was not updated.".format(
                                        resource_type,
                                        resource_selector_value,
                                    )
                                )
                            else:
                                ret["comment"] = (
                                    "The {} resource was not updated.".format(
                                        resource_type,
                                    )
                                )
        if ret["result"] is None:
            ## add
            if __opts__["test"]:
                ret["result"] = True
            else:
                ## add resource
                if "resource_selector" in zonecfg_kwargs:
                    del zonecfg_kwargs["resource_selector"]
                zonecfg_res = __salt__["zonecfg.add_resource"](**zonecfg_kwargs)
                ret["result"] = zonecfg_res["status"]
                if "message" in zonecfg_res:
                    ret["comment"] = zonecfg_res["message"]

            if ret["result"]:
                ret["changes"][resource_type] = {}
                if resource_selector_property:
                    ret["changes"][resource_type][resource_selector_value] = {}
                for key in kwargs if ret["result"] else []:
                    if resource_selector_property:
                        ret["changes"][resource_type][resource_selector_value][key] = (
                            _parse_value(kwargs[key])
                        )
                    else:
                        ret["changes"][resource_type][key] = _parse_value(kwargs[key])
                if ret["comment"] == "":
                    ret["comment"] = "The {} resource {} was added.".format(
                        resource_type,
                        resource_selector_value,
                    )
            elif ret["comment"] == "":
                ret["comment"] = "The {} resource {} was not added.".format(
                    resource_type,
                    resource_selector_value,
                )
    else:
        ## zone does not exist
        ret["result"] = False
        ret["comment"] = (
            "The zone {} is not in the configured, installed, or booted state.".format(
                name
            )
        )

    return ret


def resource_absent(
    name, resource_type, resource_selector_property, resource_selector_value
):
    """
    Ensure resource is absent

    name : string
        name of the zone
    resource_type : string
        type of resource
    resource_selector_property : string
        unique resource identifier
    resource_selector_value : string
        value for resource selection

    .. warning::
        Both resource_selector_property and resource_selector_value must be provided, some properties
        like ```name``` are already reserved by salt in there states.

    .. note::
        You can set both resource_selector_property and resource_selector_value to None for
        resources that do not require them.

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

    # sanitize input
    if resource_selector_property:
        resource_selector_value = _parse_value(resource_selector_value)
    else:
        resource_selector_value = None

    zones = __salt__["zoneadm.list"](installed=True, configured=True)
    if name in zones:
        ## zone exists
        zonecfg = __salt__["zonecfg.info"](name, show_all=True)
        if resource_type in zonecfg:
            for resource in zonecfg[resource_type]:
                if __opts__["test"]:
                    ret["result"] = True
                elif not resource_selector_property:
                    zonecfg_res = __salt__["zonecfg.remove_resource"](
                        zone=name,
                        resource_type=resource_type,
                        resource_key=None,
                        resource_value=None,
                    )
                    ret["result"] = zonecfg_res["status"]
                    if zonecfg_res["status"]:
                        ret["changes"][resource_type] = "removed"
                        if ret["comment"] == "":
                            ret["comment"] = "The {} resource was removed.".format(
                                resource_type,
                            )
                    elif "messages" in zonecfg_res:
                        ret["comment"] = zonecfg_res["message"]
                    else:
                        ret["comment"] = "The {} resource was not removed.".format(
                            resource_type,
                        )
                elif resource[resource_selector_property] == resource_selector_value:
                    zonecfg_res = __salt__["zonecfg.remove_resource"](
                        zone=name,
                        resource_type=resource_type,
                        resource_key=resource_selector_property,
                        resource_value=resource_selector_value,
                    )
                    ret["result"] = zonecfg_res["status"]
                    if zonecfg_res["status"]:
                        ret["changes"][resource_type] = {}
                        ret["changes"][resource_type][
                            resource_selector_value
                        ] = "removed"
                        if ret["comment"] == "":
                            ret["comment"] = "The {} resource {} was removed.".format(
                                resource_type,
                                resource_selector_value,
                            )
                    elif "messages" in zonecfg_res:
                        ret["comment"] = zonecfg_res["message"]
                    else:
                        ret["comment"] = "The {} resource {} was not removed.".format(
                            resource_type,
                            resource_selector_value,
                        )

        # resource already absent
        if ret["result"] is None:
            ret["result"] = True
            ret["comment"] = "The {} resource {} was absent.".format(
                resource_type,
                resource_selector_value,
            )
    else:
        ## zone does not exist
        ret["result"] = False
        ret["comment"] = (
            "The zone {} is not in the configured, installed, or booted state.".format(
                name
            )
        )

    return ret


def booted(name, single=False):
    """
    Ensure zone is booted

    name : string
        name of the zone
    single : boolean
        boot in single usermode

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

    zones = __salt__["zoneadm.list"](installed=True)
    if name in zones:
        ## zone exists
        if zones[name]["state"] == "running":
            ## zone is running
            ret["result"] = True
            ret["comment"] = f"Zone {name} already booted"
        else:
            ## try and boot the zone
            if not __opts__["test"]:
                zoneadm_res = __salt__["zoneadm.boot"](name, single)
            if __opts__["test"] or zoneadm_res["status"]:
                ret["result"] = True
                ret["changes"][name] = "booted"
                ret["comment"] = f"Zone {name} booted"
            else:
                ret["result"] = False
                ret["comment"] = f"Failed to boot {name}"
    else:
        ## zone does not exist
        ret["comment"] = []
        ret["comment"].append(
            f"The zone {name} is not in the installed or booted state."
        )
        for zone in zones:
            if zones[zone]["uuid"] == name:
                ret["comment"].append(
                    "The zone {} has a uuid of {}, please use the zone name instead!".format(
                        zone,
                        name,
                    )
                )

        ret["result"] = False
        ret["comment"] = "\n".join(ret["comment"])

    return ret


def halted(name, graceful=True):
    """
    Ensure zone is halted

    name : string
        name of the zone
    graceful : boolean
        use shutdown instead of halt if true

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

    zones = __salt__["zoneadm.list"](installed=True)
    if name in zones:
        ## zone exists
        if zones[name]["state"] != "running":
            ## zone is not running
            ret["result"] = True
            ret["comment"] = f"Zone {name} already halted"
        else:
            ## try and halt the zone
            if not __opts__["test"]:
                zoneadm_res = (
                    __salt__["zoneadm.shutdown"](name)
                    if graceful
                    else __salt__["zoneadm.halt"](name)
                )
            if __opts__["test"] or zoneadm_res["status"]:
                ret["result"] = True
                ret["changes"][name] = "halted"
                ret["comment"] = f"Zone {name} halted"
            else:
                ret["result"] = False
                ret["comment"] = f"Failed to halt {name}"
    else:
        ## zone does not exist
        ret["comment"] = []
        ret["comment"].append(f"The zone {name} is not in the installed state.")
        for zone in zones:
            if zones[zone]["uuid"] == name:
                ret["comment"].append(
                    "The zone {} has a uuid of {}, please use the zone name instead!".format(
                        zone,
                        name,
                    )
                )
        ## note: a non existing zone is not running, we do not consider this a failure
        ret["result"] = True
        ret["comment"] = "\n".join(ret["comment"])

    return ret


def export(name, path, replace=False):
    """
    Export a zones configuration

    name : string
        name of the zone
    path : string
        path of file to export too.
    replace : boolean
        replace the file if it exists

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

    zones = __salt__["zoneadm.list"](installed=True, configured=True)
    if name in zones:
        ## zone exists
        if __opts__["test"]:
            ## pretend we did the correct thing
            ret["result"] = True
            ret["comment"] = "Zone configartion for {} exported to {}".format(
                name,
                path,
            )
            ret["changes"][name] = "exported"
            if __salt__["file.file_exists"](path) and not replace:
                ret["result"] = False
                ret["changes"] = {}
                ret["comment"] = (
                    "File {} exists, zone configuration for {} not exported.".format(
                        path,
                        name,
                    )
                )
        else:
            ## export and update file
            cfg_tmp = salt.utils.files.mkstemp()
            __salt__["zonecfg.export"](name, cfg_tmp)
            if not __salt__["file.file_exists"](path):
                ## move cfg_tmp to path
                try:
                    __salt__["file.move"](cfg_tmp, path)
                except CommandExecutionError:
                    if __salt__["file.file_exists"](cfg_tmp):
                        __salt__["file.remove"](cfg_tmp)
                    ret["result"] = False
                    ret["comment"] = (
                        "Unable to export zone configuration for {} to {}!".format(
                            name,
                            path,
                        )
                    )
                else:
                    ret["result"] = True
                    ret["comment"] = (
                        "Zone configuration for {} was exported to {}.".format(
                            name,
                            path,
                        )
                    )
                    ret["changes"][name] = "exported"
            else:
                cfg_diff = __salt__["file.get_diff"](path, cfg_tmp)
                if not cfg_diff:
                    ret["result"] = True
                    ret["comment"] = (
                        "Zone configuration for {} was already exported to {}.".format(
                            name, path
                        )
                    )
                    if __salt__["file.file_exists"](cfg_tmp):
                        __salt__["file.remove"](cfg_tmp)
                else:
                    if replace:
                        try:
                            __salt__["file.move"](cfg_tmp, path)
                        except CommandExecutionError:
                            if __salt__["file.file_exists"](cfg_tmp):
                                __salt__["file.remove"](cfg_tmp)
                            ret["result"] = False
                            ret["comment"] = (
                                "Unable to be re-export zone configuration for {}"
                                " to {}!".format(
                                    name,
                                    path,
                                )
                            )
                        else:
                            ret["result"] = True
                            ret["comment"] = (
                                "Zone configuration for {} was re-exported to {}.".format(
                                    name,
                                    path,
                                )
                            )
                            ret["changes"][name] = "exported"
                    else:
                        ret["result"] = False
                        ret["comment"] = (
                            "Zone configuration for {} is different from the one"
                            " exported to {}!".format(name, path)
                        )
                        if __salt__["file.file_exists"](cfg_tmp):
                            __salt__["file.remove"](cfg_tmp)
    else:
        ## zone does not exist
        ret["comment"] = []
        ret["comment"].append(f"The zone {name} does not exist.")
        for zone in zones:
            if zones[zone]["uuid"] == name:
                ret["comment"].append(
                    "The zone {} has a uuid of {}, please use the zone name instead!".format(
                        name,
                        path,
                    )
                )

        ret["result"] = False
        ret["comment"] = "\n".join(ret["comment"])

    return ret


def import_(name, path, mode="import", nodataset=False, brand_opts=None):
    """
    Import a zones configuration

    name : string
        name of the zone
    path : string
        path of the configuration file to import
    mode : string
        either import, install, or attach
    nodataset : boolean
        do not create a ZFS file system
    brand_opts : boolean
        brand specific options to pass

    .. note::
        The mode argument can be set to ``import``, ``install``, or ``attach``.
        ``import``: will only import the configuration
        ``install``: will import and then try to install the zone
        ``attach``: will import and then try to attach of the zone

    .. code-block:: yaml

        omipkg1:
          zone.import:
            - path: /foo/bar/baz
    """
    ret = {"name": name, "changes": {}, "result": None, "comment": ""}

    zones = __salt__["zoneadm.list"](installed=True, configured=True)
    if name not in zones:
        if __opts__["test"]:
            ret["result"] = True
            ret["comment"] = "Zone {} was imported from {}.".format(
                name,
                path,
            )
            ret["changes"][name] = "imported"
        else:
            if __salt__["file.file_exists"](path):
                res_import = __salt__["zonecfg.import"](name, path)
                if not res_import["status"]:
                    ret["result"] = False
                    ret["comment"] = f"Unable to import zone configuration for {name}!"
                else:
                    ret["result"] = True
                    ret["changes"][name] = "imported"
                    ret["comment"] = "Zone {} was imported from {}.".format(
                        name,
                        path,
                    )
                    if mode.lower() == "attach":
                        res_attach = __salt__["zoneadm.attach"](name, False, brand_opts)
                        ret["result"] = res_attach["status"]
                        if res_attach["status"]:
                            ret["changes"][name] = "attached"
                            ret["comment"] = "Zone {} was attached from {}.".format(
                                name,
                                path,
                            )
                        else:
                            ret["comment"] = []
                            ret["comment"].append(
                                "Failed to attach zone {} from {}!".format(
                                    name,
                                    path,
                                )
                            )
                            if "message" in res_attach:
                                ret["comment"].append(res_attach["message"])
                            ret["comment"] = "\n".join(ret["comment"])
                    if mode.lower() == "install":
                        res_install = __salt__["zoneadm.install"](
                            name, nodataset, brand_opts
                        )
                        ret["result"] = res_install["status"]
                        if res_install["status"]:
                            ret["changes"][name] = "installed"
                            ret["comment"] = "Zone {} was installed from {}.".format(
                                name,
                                path,
                            )
                        else:
                            ret["comment"] = []
                            ret["comment"].append(
                                "Failed to install zone {} from {}!".format(
                                    name,
                                    path,
                                )
                            )
                            if "message" in res_install:
                                ret["comment"].append(res_install["message"])
                            ret["comment"] = "\n".join(ret["comment"])
            else:
                ret["result"] = False
                ret["comment"] = f"The file {path} does not exists, unable to import!"
    else:
        ## zone exist
        ret["result"] = True
        ret["comment"] = "Zone {} already exists, not importing configuration.".format(
            name
        )

    return ret


def present(name, brand, zonepath, properties=None, resources=None):
    """
    Ensure a zone with certain properties and resources

    name : string
        name of the zone
    brand : string
        brand of the zone
    zonepath : string
        path of the zone
    properties : list of key-value pairs
        dict of properties
    resources : list of key-value pairs
        dict of resources

    .. note::
        If the zone does not exist it will not be installed.
        You can use the ```zone.installed``` state for this.

    .. note::
        Default resource selectors:
            - fs: dir
            - net: mac-addr
            - device: match
            - rctl: name
            - attr: name
            - dataset: name
            - admin: user

    .. warning::
        Properties and resource will not be removed when they
        are absent from the state!

        For properties, simple set them to ```None```.

        For resources, add the ```resource_prune``` property
        and set it to ```True```. Also specify the
        ```resource_selector_property``` if the default is not
        the one you want.

    """
    ret = {"name": name, "changes": {}, "result": None, "comment": []}

    ## sanitize defaults
    if not properties:
        properties = []
    if not resources:
        resources = []
    properties.append(OrderedDict({"brand": brand}))
    properties.append(OrderedDict({"zonepath": zonepath}))

    zones = __salt__["zoneadm.list"](installed=True, configured=True)

    ## test mode only has limited support
    if __opts__["test"]:
        ret["result"] = None
        ret["comment"].append(
            f"Cannot determine of changes would happen to the zone {name}."
        )

    ## create zone if needed
    if name not in zones:
        if __opts__["test"]:
            ## we pretend we created the zone
            res_create = {"status": True}
            ret["comment"] = []
        else:
            ## create and install
            res_create = __salt__["zonecfg.create"](name, brand, zonepath)
        if res_create["status"]:
            ret["result"] = True
            ret["changes"][name] = "created"
            ret["comment"].append(f"The zone {name} was created.")

    if not __opts__["test"]:
        ret["result"] = True
        if isinstance(properties, list):
            for prop in properties:
                if not isinstance(prop, OrderedDict) or len(prop) != 1:
                    log.warning("zone.present - failed to parse property: %s", prop)
                    continue
                for key, value in prop.items():
                    res = None
                    if not value:
                        res = property_absent(name, key)
                    elif value:
                        res = property_present(name, key, value)
                    if res:
                        ret["result"] = ret["result"] if res["result"] else False
                        ret["comment"].append(res["comment"])
                        if len(res["changes"]) > 0:
                            if "property" not in ret["changes"]:
                                ret["changes"]["property"] = {}
                            ret["changes"]["property"] = merge_dict(
                                ret["changes"]["property"], res["changes"]
                            )
        if isinstance(resources, list):
            for resource in resources:
                if not isinstance(prop, OrderedDict) or len(prop) != 1:
                    log.warning("zone.present - failed to parse resource: %s", resource)
                    continue
                for key, value in resource.items():
                    zonecfg = __salt__["zonecfg.info"](name, show_all=True)
                    resource_cfg = {}
                    resource_cfg["resource_type"] = key
                    if isinstance(value, list):
                        for respv in value:
                            resource_cfg.update(dict(respv))

                    resource_prune = False
                    resource_selector_property = None
                    if "resource_prune" in resource_cfg:
                        resource_prune = resource_cfg["resource_prune"]
                        del resource_cfg["resource_prune"]
                    if "resource_selector_property" in resource_cfg:
                        resource_selector_property = resource_cfg[
                            "resource_selector_property"
                        ]
                        del resource_cfg["resource_selector_property"]
                    if (
                        not resource_selector_property
                        and key in _zonecfg_resource_default_selectors
                    ):
                        resource_selector_property = (
                            _zonecfg_resource_default_selectors[key]
                        )

                    res = None
                    if resource_prune:
                        res = resource_absent(
                            name,
                            resource_cfg["resource_type"],
                            resource_selector_property=resource_selector_property,
                            resource_selector_value=(
                                resource_cfg[resource_selector_property]
                                if resource_selector_property
                                else None
                            ),
                        )
                    else:
                        resource_cfg["resource_selector_property"] = (
                            resource_selector_property
                        )
                        if resource_selector_property in resource_cfg:
                            resource_cfg["resource_selector_value"] = resource_cfg[
                                resource_selector_property
                            ]
                        else:
                            resource_cfg["resource_selector_value"] = None
                        resource_cfg["name"] = (
                            name  # we do this last because name can also be a attrib value
                        )
                        res = resource_present(**resource_cfg)
                    if res:
                        ret["result"] = ret["result"] if res["result"] else False
                        ret["comment"].append(res["comment"])
                        if len(res["changes"]) > 0:
                            if "resource" not in ret["changes"]:
                                ret["changes"]["resource"] = {}
                            ret["changes"]["resource"] = merge_dict(
                                ret["changes"]["resource"], res["changes"]
                            )

    if isinstance(ret["comment"], list):
        ret["comment"] = "\n".join(ret["comment"])

    return ret


def absent(name, uninstall=False):
    """
    Ensure a zone is absent

    name : string
        name of the zone
    uninstall : boolean
        when true, uninstall instead of detaching the zone first.

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

    zones = __salt__["zoneadm.list"](installed=True, configured=True)
    if name in zones:
        if __opts__["test"]:
            ret["result"] = True
            ret["changes"][name] = "removed"
            ret["comment"] = f"Zone {name} was removed."
        else:
            ret["result"] = True
            if uninstall and zones[name]["state"] in ["running", "installed"]:
                res_halt = __salt__["zoneadm.halt"](name)
                res_uninstall = __salt__["zoneadm.uninstall"](name)
                ret["result"] = res_uninstall["status"]
                if ret["result"]:
                    ret["changes"][name] = "uninstalled"
                    ret["comment"] = f"The zone {name} was uninstalled."
                else:
                    ret["comment"] = []
                    ret["comment"].append(f"Failed to uninstall zone {name}!")
                    if "message" in res_uninstall:
                        ret["comment"].append(res_uninstall["message"])
                    ret["comment"] = "\n".join(ret["comment"])
            elif zones[name]["state"] == "installed":
                res_detach = __salt__["zoneadm.detach"](name)
                ret["result"] = res_detach["status"]
                if ret["result"]:
                    ret["changes"][name] = "detached"
                    ret["comment"] = f"The zone {name} was detached."
                else:
                    ret["comment"] = []
                    ret["comment"].append(f"Failed to detach zone {name}!")
                    if "message" in res_detach:
                        ret["comment"].append(res_detach["message"])
                    ret["comment"] = "\n".join(ret["comment"])
            if ret["result"]:
                res_delete = __salt__["zonecfg.delete"](name)
                ret["result"] = res_delete["status"]
                if ret["result"]:
                    ret["changes"][name] = "deleted"
                    ret["comment"] = f"The zone {name} was delete."
                else:
                    ret["comment"] = []
                    ret["comment"].append(f"Failed to delete zone {name}!")
                    if "message" in res_delete:
                        ret["comment"].append(res_delete["message"])
                    ret["comment"] = "\n".join(ret["comment"])
    else:
        ret["result"] = True
        ret["comment"] = f"Zone {name} does not exist."

    return ret


def attached(name, force=False):
    """
    Ensure zone is attached

    name : string
        name of the zone
    force : boolean
        force attach the zone

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

    zones = __salt__["zoneadm.list"](installed=True, configured=True)
    if name in zones:
        if zones[name]["state"] == "configured":
            if __opts__["test"]:
                res_attach = {"status": True}
            else:
                res_attach = __salt__["zoneadm.attach"](name, force)
            ret["result"] = res_attach["status"]
            if ret["result"]:
                ret["changes"][name] = "attached"
                ret["comment"] = f"The zone {name} was attached."
            else:
                ret["comment"] = []
                ret["comment"].append(f"Failed to attach zone {name}!")
                if "message" in res_attach:
                    ret["comment"].append(res_attach["message"])
                ret["comment"] = "\n".join(ret["comment"])
        else:
            ret["result"] = True
            ret["comment"] = f"zone {name} already attached."
    else:
        ret["result"] = False
        ret["comment"] = f"zone {name} is not configured!"

    return ret


def detached(name):
    """
    Ensure zone is detached

    name : string
        name of the zone

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

    zones = __salt__["zoneadm.list"](installed=True, configured=True)
    if name in zones:
        if zones[name]["state"] != "configured":
            if __opts__["test"]:
                res_detach = {"status": True}
            else:
                res_detach = __salt__["zoneadm.detach"](name)
            ret["result"] = res_detach["status"]
            if ret["result"]:
                ret["changes"][name] = "detached"
                ret["comment"] = f"The zone {name} was detached."
            else:
                ret["comment"] = []
                ret["comment"].append(f"Failed to detach zone {name}!")
                if "message" in res_detach:
                    ret["comment"].append(res_detach["message"])
                ret["comment"] = "\n".join(ret["comment"])
        else:
            ret["result"] = True
            ret["comment"] = f"zone {name} already detached."
    else:
        ## note: a non existing zone is not attached, we do not consider this a failure
        ret["result"] = True
        ret["comment"] = f"zone {name} is not configured!"

    return ret


def installed(name, nodataset=False, brand_opts=None):
    """
    Ensure zone is installed

    name : string
        name of the zone
    nodataset : boolean
        do not create a ZFS file system
    brand_opts : boolean
        brand specific options to pass

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

    zones = __salt__["zoneadm.list"](installed=True, configured=True)
    if name in zones:
        if zones[name]["state"] == "configured":
            if __opts__["test"]:
                res_install = {"status": True}
            else:
                res_install = __salt__["zoneadm.install"](name, nodataset, brand_opts)
            ret["result"] = res_install["status"]
            if ret["result"]:
                ret["changes"][name] = "installed"
                ret["comment"] = f"The zone {name} was installed."
            else:
                ret["comment"] = []
                ret["comment"].append(f"Failed to install zone {name}!")
                if "message" in res_install:
                    ret["comment"].append(res_install["message"])
                ret["comment"] = "\n".join(ret["comment"])
        else:
            ret["result"] = True
            ret["comment"] = f"zone {name} already installed."
    else:
        ret["result"] = False
        ret["comment"] = f"zone {name} is not configured!"

    return ret


def uninstalled(name):
    """
    Ensure zone is uninstalled

    name : string
        name of the zone

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

    zones = __salt__["zoneadm.list"](installed=True, configured=True)
    if name in zones:
        if zones[name]["state"] != "configured":
            if __opts__["test"]:
                res_uninstall = {"status": True}
            else:
                res_uninstall = __salt__["zoneadm.uninstall"](name)
            ret["result"] = res_uninstall["status"]
            if ret["result"]:
                ret["changes"][name] = "uninstalled"
                ret["comment"] = f"The zone {name} was uninstalled."
            else:
                ret["comment"] = []
                ret["comment"].append(f"Failed to uninstall zone {name}!")
                if "message" in res_uninstall:
                    ret["comment"].append(res_uninstall["message"])
                ret["comment"] = "\n".join(ret["comment"])
        else:
            ret["result"] = True
            ret["comment"] = f"zone {name} already uninstalled."
    else:
        ## note: a non existing zone is not installed, we do not consider this a failure
        ret["result"] = True
        ret["comment"] = f"zone {name} is not configured!"

    return ret
