#!/usr/bin/python
# -*- coding: utf-8 -*-

# (c) 2013, Johan Wiren <johan.wiren.se@gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
#

DOCUMENTATION = '''
---
module: zfs
short_description: Manage zfs
description:
  - Manages ZFS file systems on Solaris and FreeBSD. Can manage file systems, volumes and snapshots. See zfs(1M) for more information about the properties.
version_added: "1.1"
options:
  name:
    description:
      - File system, snapshot or volume name e.g. C(rpool/myfs)
    required: true
  state:
    description:
      - Whether to create (C(present)), or remove (C(absent)) a file system, snapshot or volume.
    required: true
    choices: [present, absent]
  aclinherit:
    description:
      - The aclinherit property.
    required: False
    choices: [discard,noallow,restricted,passthrough,passthrough-x]
  aclmode:
    description:
      - The aclmode property.
    required: False
    choices: [discard,groupmask,passthrough]
  atime:
    description:
      - The atime property.
    required: False
    choices: ['on','off']
  canmount:
    description:
      - The canmount property.
    required: False
    choices: ['on','off','noauto']
  casesensitivity:
    description:
      - The casesensitivity property.
    required: False
    choices: [sensitive,insensitive,mixed]
  checksum:
    description:
      - The checksum property.
    required: False
    choices: ['on','off',fletcher2,fletcher4,sha256]
  compression:
    description:
      - The compression property.
    required: False
    choices: ['on','off',lzjb,gzip,gzip-1,gzip-2,gzip-3,gzip-4,gzip-5,gzip-6,gzip-7,gzip-8,gzip-9,lz4,zle]
  copies:
    description:
      - The copies property.
    required: False
    choices: [1,2,3]
  dedup:
    description:
      - The dedup property.
    required: False
    choices: ['on','off']
  devices:
    description:
      - The devices property.
    required: False
    choices: ['on','off']
  exec:
    description:
      - The exec property.
    required: False
    choices: ['on','off']
  jailed:
    description:
      - The jailed property.
    required: False
    choices: ['on','off']
  logbias:
    description:
      - The logbias property.
    required: False
    choices: [latency,throughput]
  mountpoint:
    description:
      - The mountpoint property.
    required: False
  nbmand:
    description:
      - The nbmand property.
    required: False
    choices: ['on','off']
  normalization:
    description:
      - The normalization property.
    required: False
    choices: [none,formC,formD,formKC,formKD]
  origin:
    description:
      - Name of the snapshot to clone
    required: False
    version_added: "2.0"
  primarycache:
    description:
      - The primarycache property.
    required: False
    choices: [all,none,metadata]
  quota:
    description:
      - The quota property.
    required: False
  readonly:
    description:
      - The readonly property.
    required: False
    choices: ['on','off']
  recordsize:
    description:
      - The recordsize property.
    required: False
  refquota:
    description:
      - The refquota property.
    required: False
  refreservation:
    description:
      - The refreservation property.
    required: False
  reservation:
    description:
      - The reservation property.
    required: False
  secondarycache:
    description:
      - The secondarycache property.
    required: False
    choices: [all,none,metadata]
  setuid:
    description:
      - The setuid property.
    required: False
    choices: ['on','off']
  shareiscsi:
    description:
      - The shareiscsi property.
    required: False
    choices: ['on','off']
  sharenfs:
    description:
      - The sharenfs property.
    required: False
  sharesmb:
    description:
      - The sharesmb property.
    required: False
  snapdir:
    description:
      - The snapdir property.
    required: False
    choices: [hidden,visible]
  sync:
    description:
      - The sync property.
    required: False
    choices: ['standard','always','disabled']
  utf8only:
    description:
      - The utf8only property.
    required: False
    choices: ['on','off']
  volsize:
    description:
      - The volsize property.
    required: False
  volblocksize:
    description:
      - The volblocksize property.
    required: False
  vscan:
    description:
      - The vscan property.
    required: False
    choices: ['on','off']
  xattr:
    description:
      - The xattr property.
    required: False
    choices: ['on','off']
  zoned:
    description:
      - The zoned property.
    required: False
    choices: ['on','off']
author: "Johan Wiren (@johanwiren)"
'''

EXAMPLES = '''
# Create a new file system called myfs in pool rpool
- zfs: name=rpool/myfs state=present

# Create a new volume called myvol in pool rpool. 
- zfs: name=rpool/myvol state=present volsize=10M

# Create a snapshot of rpool/myfs file system.
- zfs: name=rpool/myfs@mysnapshot state=present

# Create a new file system called myfs2 with snapdir enabled
- zfs: name=rpool/myfs2 state=present snapdir=enabled

# Create a new file system by cloning a snapshot
- zfs: name=rpool/cloned_fs state=present origin=rpool/myfs@mysnapshot

# Destroy a filesystem
- zfs: name=rpool/myfs state=absent
'''


import os

class Zfs(object):
    def __init__(self, module, name, properties):
        self.module = module
        self.name = name
        self.properties = properties
        self.changed = False

        self.immutable_properties = [ 'casesensitivity', 'normalization', 'utf8only' ]

    def exists(self):
        cmd = [self.module.get_bin_path('zfs', True)]
        cmd.append('list')
        cmd.append('-t all')
        cmd.append(self.name)
        (rc, out, err) = self.module.run_command(' '.join(cmd))
        if rc == 0:
            return True
        else:
            return False

    def create(self):
        if self.module.check_mode:
            self.changed = True
            return
        properties = self.properties
        volsize = properties.pop('volsize', None)
        volblocksize = properties.pop('volblocksize', None)
        origin = properties.pop('origin', None)
        createparent = properties.pop('createparent', None)
        if "@" in self.name:
            action = 'snapshot'
        elif origin:
            action = 'clone'
        else:
            action = 'create'

        cmd = [self.module.get_bin_path('zfs', True)]
        cmd.append(action)

        if createparent:
            cmd.append('-p')

        if volblocksize:
            cmd.append('-b %s' % volblocksize)
        if properties:
            for prop, value in properties.iteritems():
                cmd.append('-o %s="%s"' % (prop, value))
        if volsize:
            cmd.append('-V')
            cmd.append(volsize)
        if origin:
            cmd.append(origin)
        cmd.append(self.name)
        (rc, err, out) = self.module.run_command(' '.join(cmd))
        if rc == 0:
            self.changed = True
        else:
            self.module.fail_json(msg=out)

    def destroy(self):
        if self.module.check_mode:
            self.changed = True
            return
        cmd = [self.module.get_bin_path('zfs', True)]
        cmd.append('destroy')
        cmd.append(self.name)
        (rc, err, out) = self.module.run_command(' '.join(cmd))
        if rc == 0:
            self.changed = True
        else:
            self.module.fail_json(msg=out)

    def set_property(self, prop, value):
        if self.module.check_mode:
            self.changed = True
            return
        cmd = self.module.get_bin_path('zfs', True)
        args = [cmd, 'set', prop + '=' + value, self.name]
        (rc, err, out) = self.module.run_command(args)
        if rc == 0:
            self.changed = True
        else:
            self.module.fail_json(msg=out)

    def set_properties_if_changed(self):
        current_properties = self.get_current_properties()
        for prop, value in self.properties.iteritems():
            if current_properties[prop] != value:
                if prop in self.immutable_properties:
                    self.module.fail_json(msg='Cannot change property %s after creation.' % prop)
                else:
                    self.set_property(prop, value) 

    def get_current_properties(self):
        def get_properties_by_name(propname):
            cmd = [self.module.get_bin_path('zfs', True)]
            cmd += ['get', '-H', propname, self.name]
            rc, out, err = self.module.run_command(cmd)
            return [l.split('\t')[1:3] for l in out.splitlines()]
        properties = dict(get_properties_by_name('all'))
        if 'share.*' in properties:
            # Some ZFS pools list the sharenfs and sharesmb properties
            # hierarchically as share.nfs and share.smb respectively.
            del properties['share.*']
            for p, v in get_properties_by_name('share.all'):
                alias = p.replace('.', '')  # share.nfs -> sharenfs (etc)
                properties[alias] = v
        return properties

    def run_command(self, cmd):
        progname = cmd[0]
        cmd[0] = module.get_bin_path(progname, True)
        return module.run_command(cmd)

def main():

    # FIXME: should use dict() constructor like other modules, required=False is default
    module = AnsibleModule(
        argument_spec = {
            'name':            {'required': True},
            'state':           {'required': True,  'choices':['present', 'absent']},
            'aclinherit':      {'required': False, 'choices':['discard', 'noallow', 'restricted', 'passthrough', 'passthrough-x']},
            'aclmode':         {'required': False, 'choices':['discard', 'groupmask', 'passthrough']},
            'atime':           {'required': False, 'choices':['on', 'off']},
            'canmount':        {'required': False, 'choices':['on', 'off', 'noauto']},
            'casesensitivity': {'required': False, 'choices':['sensitive', 'insensitive', 'mixed']},
            'checksum':        {'required': False, 'choices':['on', 'off', 'fletcher2', 'fletcher4', 'sha256']},
            'compression':     {'required': False, 'choices':['on', 'off', 'lzjb', 'gzip', 'gzip-1', 'gzip-2', 'gzip-3', 'gzip-4', 'gzip-5', 'gzip-6', 'gzip-7', 'gzip-8', 'gzip-9', 'lz4', 'zle']},
            'copies':          {'required': False, 'choices':['1', '2', '3']},
            'createparent':    {'required': False, 'choices':['on', 'off']},
            'dedup':           {'required': False, 'choices':['on', 'off']},
            'devices':         {'required': False, 'choices':['on', 'off']},
            'exec':            {'required': False, 'choices':['on', 'off']},
            # Not supported
            #'groupquota':      {'required': False},
            'jailed':          {'required': False, 'choices':['on', 'off']},
            'logbias':         {'required': False, 'choices':['latency', 'throughput']},
            'mountpoint':      {'required': False},
            'nbmand':          {'required': False, 'choices':['on', 'off']},
            'normalization':   {'required': False, 'choices':['none', 'formC', 'formD', 'formKC', 'formKD']},
            'origin':          {'required': False},
            'primarycache':    {'required': False, 'choices':['all', 'none', 'metadata']},
            'quota':           {'required': False},
            'readonly':        {'required': False, 'choices':['on', 'off']},
            'recordsize':      {'required': False},
            'refquota':        {'required': False},
            'refreservation':  {'required': False},
            'reservation':     {'required': False},
            'secondarycache':  {'required': False, 'choices':['all', 'none', 'metadata']},
            'setuid':          {'required': False, 'choices':['on', 'off']},
            'shareiscsi':      {'required': False, 'choices':['on', 'off']},
            'sharenfs':        {'required': False},
            'sharesmb':        {'required': False},
            'snapdir':         {'required': False, 'choices':['hidden', 'visible']},
            'sync':            {'required': False, 'choices':['standard', 'always', 'disabled']},
            # Not supported
            #'userquota':       {'required': False},
            'utf8only':        {'required': False, 'choices':['on', 'off']},
            'volsize':         {'required': False},
            'volblocksize':    {'required': False},
            'vscan':           {'required': False, 'choices':['on', 'off']},
            'xattr':           {'required': False, 'choices':['on', 'off']},
            'zoned':           {'required': False, 'choices':['on', 'off']},
            },
        supports_check_mode=True
        )

    state = module.params.pop('state')
    name = module.params.pop('name')

    # Get all valid zfs-properties
    properties = dict()
    for prop, value in module.params.iteritems():
        if prop in ['CHECKMODE']:
            continue
        if value:
            properties[prop] = value

    result = {}
    result['name'] = name
    result['state'] = state

    zfs = Zfs(module, name, properties)

    if state == 'present':
        if zfs.exists():
            zfs.set_properties_if_changed()
        else:
            zfs.create()

    elif state == 'absent':
        if zfs.exists():
            zfs.destroy()

    result.update(zfs.properties)
    result['changed'] = zfs.changed
    module.exit_json(**result)

# import module snippets
from ansible.module_utils.basic import *
main()
