Repository: ansible/ansible-modules-extras

Issue:
Can't localise inherited ZFS properties because of over-eager impotency
##### Issue Type:
- Feature Idea
##### Ansible Version:

```
ansible 1.9.3
  configured module search path = None
```
##### Environment:

Solaris 11.
##### Summary:

Many ZFS properties are inheritable from the parent filesystem.

Ansible does not distinguish between local, default, and inherited values.  This makes it impossible to set the local value of a property when the inherited value is already as desired.

It would be possible to modify the module so the user can choose whether they want to ignore inherited values or not, but this is probably overkill; if the user was happy with the inherited value, they probably wouldn't have added a task to their play to set it.  If this seems reasonable, I can work on a patch.
##### Steps To Reproduce:

``` yaml
- zfs: state=present setuid=on  name=rpool/export/home/trusteduser 
- zfs: state=present setuid=off name=rpool/export/home
```
##### Expected Results:

```
> zfs get -r setuid rpool/export
NAME                           PROPERTY  VALUE  SOURCE
rpool/export                   setuid    on     default
rpool/export/home              setuid    off    local
rpool/export/home/trusteduser  setuid    on     local
rpool/export/home/otheruser    setuid    off    inherited from rpool/export/home
```
##### Actual Results:

```
> zfs get -r setuid rpool/export
NAME                           PROPERTY  VALUE  SOURCE
rpool/export                   setuid    on     default
rpool/export/home              setuid    off    local
rpool/export/home/trusteduser  setuid    off    inherited from rpool/export/home
rpool/export/home/otheruser    setuid    off    inherited from rpool/export/home
```



Discussion:
Hi @mavit -- thanks for submitting this feature idea.

@johanwiren -- pinging you to let you know about this feature request for the zfs module. It looks like @mavit might be willing to implement the feature himself and submit a PR -- if you could give him some feedback on his suggestions, that would be great.

Thanks! :)

[module: system/zfs.py]

Isn't this the exact same behaviour you get when running the zfs command in the same order? In that case I'd say that the current behaviour is what would give you the least amount of surprises. I currently do not have a zfs box at hand to verify this myself. Will try to find one soon though.

Also, reading the zfs man-page I cannot find a way to specifically set a property as "local" if its value at the same time matches that of the inherited one.

Can you elaborate on how you would implement this? Or post a PR if you have working code.


Resolving patch:
diff --git a/system/zfs.py b/system/zfs.py
--- a/system/zfs.py
+++ b/system/zfs.py
@@ -24,7 +24,7 @@
 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.
+  - Manages ZFS file systems, volumes, clones and snapshots.
 version_added: "1.1"
 options:
   name:
@@ -33,192 +33,30 @@
     required: true
   state:
     description:
-      - Whether to create (C(present)), or remove (C(absent)) a file system, snapshot or volume.
+      - Whether to create (C(present)), or remove (C(absent)) a 
+        file system, snapshot or volume. All parents/children
+        will be created/destroyed as needed to reach the desired state.
+    choices: ['present', 'absent']
     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:
+      - Snapshot from which to create a clone
+    default: null
+    required: false
+  key_value:
     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']
+      - The C(zfs) module takes key=value pairs for zfs properties to be set. See the zfs(8) man page for more information.
+    default: null
+    required: false
+
 author: "Johan Wiren (@johanwiren)"
 '''
 
 EXAMPLES = '''
-# Create a new file system called myfs in pool rpool
-- zfs: name=rpool/myfs state=present
+# Create a new file system called myfs in pool rpool with the setuid property turned off
+- zfs: name=rpool/myfs state=present setuid=off
 
-# Create a new volume called myvol in pool rpool. 
+# 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.
@@ -237,20 +75,33 @@
 
 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' ]
+        self.is_solaris = os.uname()[0] == 'SunOS'
+        self.pool = name.split('/')[0]
+        self.zfs_cmd = module.get_bin_path('zfs', True)
+        self.zpool_cmd = module.get_bin_path('zpool', True)
+        self.enhanced_sharing = self.check_enhanced_sharing()
+
+    def check_enhanced_sharing(self):
+        if os.uname()[0] == 'SunOS':
+            cmd = [self.zpool_cmd]
+            cmd.extend(['get', 'version'])
+            cmd.append(self.pool)
+            (rc, out, err) = self.module.run_command(cmd, check_rc=True)
+            version = out.splitlines()[-1].split()[2]
+            if int(version) >= 34:
+                return True
+        return False
 
     def exists(self):
-        cmd = [self.module.get_bin_path('zfs', True)]
-        cmd.append('list')
-        cmd.append('-t all')
-        cmd.append(self.name)
+        cmd = [self.zfs_cmd, 'list', '-t', 'all', self.name]
         (rc, out, err) = self.module.run_command(' '.join(cmd))
         if rc == 0:
             return True
@@ -265,7 +116,8 @@ def create(self):
         volsize = properties.pop('volsize', None)
         volblocksize = properties.pop('volblocksize', None)
         origin = properties.pop('origin', None)
-        createparent = properties.pop('createparent', None)
+        cmd = [self.zfs_cmd]
+
         if "@" in self.name:
             action = 'snapshot'
         elif origin:
@@ -273,135 +125,82 @@ def create(self):
         else:
             action = 'create'
 
-        cmd = [self.module.get_bin_path('zfs', True)]
         cmd.append(action)
 
-        if createparent:
-            cmd.append('-p')
+        if action in ['create', 'clone']:
+            cmd += ['-p']
 
+        if volsize:
+            cmd += ['-V', volsize]
         if volblocksize:
-            cmd.append('-b %s' % volblocksize)
+            cmd += ['-b', 'volblocksize']
         if properties:
             for prop, value in properties.iteritems():
-                cmd.append('-o %s="%s"' % (prop, value))
-        if volsize:
-            cmd.append('-V')
-            cmd.append(volsize)
+                cmd += ['-o', '%s="%s"' % (prop, value)]
         if origin:
             cmd.append(origin)
         cmd.append(self.name)
-        (rc, err, out) = self.module.run_command(' '.join(cmd))
+        (rc, out, err) = self.module.run_command(' '.join(cmd))
         if rc == 0:
             self.changed = True
         else:
-            self.module.fail_json(msg=out)
+            self.module.fail_json(msg=err)
 
     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))
+        cmd = [self.zfs_cmd, 'destroy', '-R', self.name]
+        (rc, out, err) = self.module.run_command(' '.join(cmd))
         if rc == 0:
             self.changed = True
         else:
-            self.module.fail_json(msg=out)
+            self.module.fail_json(msg=err)
 
     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)
+        cmd = [self.zfs_cmd, 'set', prop + '=' + str(value), self.name]
+        (rc, out, err) = self.module.run_command(cmd)
         if rc == 0:
             self.changed = True
         else:
-            self.module.fail_json(msg=out)
+            self.module.fail_json(msg=err)
 
     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) 
+            if current_properties.get(prop, None) != value:
+                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
+        cmd = [self.zfs_cmd, 'get', '-H']
+        if self.enhanced_sharing:
+            cmd += ['-e']
+        cmd += ['all', self.name]
+        rc, out, err = self.module.run_command(" ".join(cmd))
+        properties = dict()
+        for prop, value, source in [l.split('\t')[1:4] for l in out.splitlines()]:
+            if source == 'local':
+                properties[prop] = value
+        # Add alias for enhanced sharing properties
+        properties['sharenfs'] = properties.get('share.nfs', None)
+        properties['sharesmb'] = properties.get('share.smb', None)
         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
+        argument_spec = dict(
+            name =         dict(type='str', required=True),
+            state =        dict(type='str', required=True, choices=['present', 'absent']),
+            # No longer used. Kept here to not interfere with zfs properties
+            createparent = dict(type='bool', required=False)
+            ),
+        supports_check_mode=True,
+        check_invalid_arguments=False
         )
 
     state = module.params.pop('state')
@@ -410,10 +209,16 @@ def main():
     # Get all valid zfs-properties
     properties = dict()
     for prop, value in module.params.iteritems():
-        if prop in ['CHECKMODE']:
-            continue
-        if value:
-            properties[prop] = value
+        # All freestyle params are zfs properties
+        if prop not in module.argument_spec:
+            # Reverse the boolification of freestyle zfs properties
+            if type(value) == bool:
+                if value is True:
+                    properties[prop] = 'on'
+                else:
+                    properties[prop] = 'off'
+            else:
+                properties[prop] = value
 
     result = {}
     result['name'] = name


Test patch: