summaryrefslogtreecommitdiffstats
path: root/lib/ansible/modules/group.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ansible/modules/group.py')
-rw-r--r--lib/ansible/modules/group.py662
1 files changed, 662 insertions, 0 deletions
diff --git a/lib/ansible/modules/group.py b/lib/ansible/modules/group.py
new file mode 100644
index 0000000..109a161
--- /dev/null
+++ b/lib/ansible/modules/group.py
@@ -0,0 +1,662 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2012, Stephen Fromm <sfromm@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: group
+version_added: "0.0.2"
+short_description: Add or remove groups
+requirements:
+- groupadd
+- groupdel
+- groupmod
+description:
+ - Manage presence of groups on a host.
+ - For Windows targets, use the M(ansible.windows.win_group) module instead.
+options:
+ name:
+ description:
+ - Name of the group to manage.
+ type: str
+ required: true
+ gid:
+ description:
+ - Optional I(GID) to set for the group.
+ type: int
+ state:
+ description:
+ - Whether the group should be present or not on the remote host.
+ type: str
+ choices: [ absent, present ]
+ default: present
+ system:
+ description:
+ - If I(yes), indicates that the group created is a system group.
+ type: bool
+ default: no
+ local:
+ description:
+ - Forces the use of "local" command alternatives on platforms that implement it.
+ - This is useful in environments that use centralized authentication when you want to manipulate the local groups.
+ (for example, it uses C(lgroupadd) instead of C(groupadd)).
+ - This requires that these commands exist on the targeted host, otherwise it will be a fatal error.
+ type: bool
+ default: no
+ version_added: "2.6"
+ non_unique:
+ description:
+ - This option allows to change the group ID to a non-unique value. Requires C(gid).
+ - Not supported on macOS or BusyBox distributions.
+ type: bool
+ default: no
+ version_added: "2.8"
+extends_documentation_fragment: action_common_attributes
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: none
+ platform:
+ platforms: posix
+seealso:
+- module: ansible.builtin.user
+- module: ansible.windows.win_group
+author:
+- Stephen Fromm (@sfromm)
+'''
+
+EXAMPLES = '''
+- name: Ensure group "somegroup" exists
+ ansible.builtin.group:
+ name: somegroup
+ state: present
+
+- name: Ensure group "docker" exists with correct gid
+ ansible.builtin.group:
+ name: docker
+ state: present
+ gid: 1750
+'''
+
+RETURN = r'''
+gid:
+ description: Group ID of the group.
+ returned: When C(state) is 'present'
+ type: int
+ sample: 1001
+name:
+ description: Group name.
+ returned: always
+ type: str
+ sample: users
+state:
+ description: Whether the group is present or not.
+ returned: always
+ type: str
+ sample: 'absent'
+system:
+ description: Whether the group is a system group or not.
+ returned: When C(state) is 'present'
+ type: bool
+ sample: False
+'''
+
+import grp
+import os
+
+from ansible.module_utils._text import to_bytes
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.sys_info import get_platform_subclass
+
+
+class Group(object):
+ """
+ This is a generic Group manipulation class that is subclassed
+ based on platform.
+
+ A subclass may wish to override the following action methods:-
+ - group_del()
+ - group_add()
+ - group_mod()
+
+ All subclasses MUST define platform and distribution (which may be None).
+ """
+
+ platform = 'Generic'
+ distribution = None # type: str | None
+ GROUPFILE = '/etc/group'
+
+ def __new__(cls, *args, **kwargs):
+ new_cls = get_platform_subclass(Group)
+ return super(cls, new_cls).__new__(new_cls)
+
+ def __init__(self, module):
+ self.module = module
+ self.state = module.params['state']
+ self.name = module.params['name']
+ self.gid = module.params['gid']
+ self.system = module.params['system']
+ self.local = module.params['local']
+ self.non_unique = module.params['non_unique']
+
+ def execute_command(self, cmd):
+ return self.module.run_command(cmd)
+
+ def group_del(self):
+ if self.local:
+ command_name = 'lgroupdel'
+ else:
+ command_name = 'groupdel'
+ cmd = [self.module.get_bin_path(command_name, True), self.name]
+ return self.execute_command(cmd)
+
+ def _local_check_gid_exists(self):
+ if self.gid:
+ for gr in grp.getgrall():
+ if self.gid == gr.gr_gid and self.name != gr.gr_name:
+ self.module.fail_json(msg="GID '{0}' already exists with group '{1}'".format(self.gid, gr.gr_name))
+
+ def group_add(self, **kwargs):
+ if self.local:
+ command_name = 'lgroupadd'
+ self._local_check_gid_exists()
+ else:
+ command_name = 'groupadd'
+ cmd = [self.module.get_bin_path(command_name, True)]
+ for key in kwargs:
+ if key == 'gid' and kwargs[key] is not None:
+ cmd.append('-g')
+ cmd.append(str(kwargs[key]))
+ if self.non_unique:
+ cmd.append('-o')
+ elif key == 'system' and kwargs[key] is True:
+ cmd.append('-r')
+ cmd.append(self.name)
+ return self.execute_command(cmd)
+
+ def group_mod(self, **kwargs):
+ if self.local:
+ command_name = 'lgroupmod'
+ self._local_check_gid_exists()
+ else:
+ command_name = 'groupmod'
+ cmd = [self.module.get_bin_path(command_name, True)]
+ info = self.group_info()
+ for key in kwargs:
+ if key == 'gid':
+ if kwargs[key] is not None and info[2] != int(kwargs[key]):
+ cmd.append('-g')
+ cmd.append(str(kwargs[key]))
+ if self.non_unique:
+ cmd.append('-o')
+ if len(cmd) == 1:
+ return (None, '', '')
+ if self.module.check_mode:
+ return (0, '', '')
+ cmd.append(self.name)
+ return self.execute_command(cmd)
+
+ def group_exists(self):
+ # The grp module does not distinguish between local and directory accounts.
+ # It's output cannot be used to determine whether or not a group exists locally.
+ # It returns True if the group exists locally or in the directory, so instead
+ # look in the local GROUP file for an existing account.
+ if self.local:
+ if not os.path.exists(self.GROUPFILE):
+ self.module.fail_json(msg="'local: true' specified but unable to find local group file {0} to parse.".format(self.GROUPFILE))
+
+ exists = False
+ name_test = '{0}:'.format(self.name)
+ with open(self.GROUPFILE, 'rb') as f:
+ reversed_lines = f.readlines()[::-1]
+ for line in reversed_lines:
+ if line.startswith(to_bytes(name_test)):
+ exists = True
+ break
+
+ if not exists:
+ self.module.warn(
+ "'local: true' specified and group was not found in {file}. "
+ "The local group may already exist if the local group database exists somewhere other than {file}.".format(file=self.GROUPFILE))
+
+ return exists
+
+ else:
+ try:
+ if grp.getgrnam(self.name):
+ return True
+ except KeyError:
+ return False
+
+ def group_info(self):
+ if not self.group_exists():
+ return False
+ try:
+ info = list(grp.getgrnam(self.name))
+ except KeyError:
+ return False
+ return info
+
+
+# ===========================================
+
+class SunOS(Group):
+ """
+ This is a SunOS Group manipulation class. Solaris doesn't have
+ the 'system' group concept.
+
+ This overrides the following methods from the generic class:-
+ - group_add()
+ """
+
+ platform = 'SunOS'
+ distribution = None
+ GROUPFILE = '/etc/group'
+
+ def group_add(self, **kwargs):
+ cmd = [self.module.get_bin_path('groupadd', True)]
+ for key in kwargs:
+ if key == 'gid' and kwargs[key] is not None:
+ cmd.append('-g')
+ cmd.append(str(kwargs[key]))
+ if self.non_unique:
+ cmd.append('-o')
+ cmd.append(self.name)
+ return self.execute_command(cmd)
+
+
+# ===========================================
+
+class AIX(Group):
+ """
+ This is a AIX Group manipulation class.
+
+ This overrides the following methods from the generic class:-
+ - group_del()
+ - group_add()
+ - group_mod()
+ """
+
+ platform = 'AIX'
+ distribution = None
+ GROUPFILE = '/etc/group'
+
+ def group_del(self):
+ cmd = [self.module.get_bin_path('rmgroup', True), self.name]
+ return self.execute_command(cmd)
+
+ def group_add(self, **kwargs):
+ cmd = [self.module.get_bin_path('mkgroup', True)]
+ for key in kwargs:
+ if key == 'gid' and kwargs[key] is not None:
+ cmd.append('id=' + str(kwargs[key]))
+ elif key == 'system' and kwargs[key] is True:
+ cmd.append('-a')
+ cmd.append(self.name)
+ return self.execute_command(cmd)
+
+ def group_mod(self, **kwargs):
+ cmd = [self.module.get_bin_path('chgroup', True)]
+ info = self.group_info()
+ for key in kwargs:
+ if key == 'gid':
+ if kwargs[key] is not None and info[2] != int(kwargs[key]):
+ cmd.append('id=' + str(kwargs[key]))
+ if len(cmd) == 1:
+ return (None, '', '')
+ if self.module.check_mode:
+ return (0, '', '')
+ cmd.append(self.name)
+ return self.execute_command(cmd)
+
+
+# ===========================================
+
+class FreeBsdGroup(Group):
+ """
+ This is a FreeBSD Group manipulation class.
+
+ This overrides the following methods from the generic class:-
+ - group_del()
+ - group_add()
+ - group_mod()
+ """
+
+ platform = 'FreeBSD'
+ distribution = None
+ GROUPFILE = '/etc/group'
+
+ def group_del(self):
+ cmd = [self.module.get_bin_path('pw', True), 'groupdel', self.name]
+ return self.execute_command(cmd)
+
+ def group_add(self, **kwargs):
+ cmd = [self.module.get_bin_path('pw', True), 'groupadd', self.name]
+ if self.gid is not None:
+ cmd.append('-g')
+ cmd.append(str(self.gid))
+ if self.non_unique:
+ cmd.append('-o')
+ return self.execute_command(cmd)
+
+ def group_mod(self, **kwargs):
+ cmd = [self.module.get_bin_path('pw', True), 'groupmod', self.name]
+ info = self.group_info()
+ cmd_len = len(cmd)
+ if self.gid is not None and int(self.gid) != info[2]:
+ cmd.append('-g')
+ cmd.append(str(self.gid))
+ if self.non_unique:
+ cmd.append('-o')
+ # modify the group if cmd will do anything
+ if cmd_len != len(cmd):
+ if self.module.check_mode:
+ return (0, '', '')
+ return self.execute_command(cmd)
+ return (None, '', '')
+
+
+class DragonFlyBsdGroup(FreeBsdGroup):
+ """
+ This is a DragonFlyBSD Group manipulation class.
+ It inherits all behaviors from FreeBsdGroup class.
+ """
+
+ platform = 'DragonFly'
+
+
+# ===========================================
+
+class DarwinGroup(Group):
+ """
+ This is a Mac macOS Darwin Group manipulation class.
+
+ This overrides the following methods from the generic class:-
+ - group_del()
+ - group_add()
+ - group_mod()
+
+ group manipulation are done using dseditgroup(1).
+ """
+
+ platform = 'Darwin'
+ distribution = None
+
+ def group_add(self, **kwargs):
+ cmd = [self.module.get_bin_path('dseditgroup', True)]
+ cmd += ['-o', 'create']
+ if self.gid is not None:
+ cmd += ['-i', str(self.gid)]
+ elif 'system' in kwargs and kwargs['system'] is True:
+ gid = self.get_lowest_available_system_gid()
+ if gid is not False:
+ self.gid = str(gid)
+ cmd += ['-i', str(self.gid)]
+ cmd += ['-L', self.name]
+ (rc, out, err) = self.execute_command(cmd)
+ return (rc, out, err)
+
+ def group_del(self):
+ cmd = [self.module.get_bin_path('dseditgroup', True)]
+ cmd += ['-o', 'delete']
+ cmd += ['-L', self.name]
+ (rc, out, err) = self.execute_command(cmd)
+ return (rc, out, err)
+
+ def group_mod(self, gid=None):
+ info = self.group_info()
+ if self.gid is not None and int(self.gid) != info[2]:
+ cmd = [self.module.get_bin_path('dseditgroup', True)]
+ cmd += ['-o', 'edit']
+ if gid is not None:
+ cmd += ['-i', str(gid)]
+ cmd += ['-L', self.name]
+ (rc, out, err) = self.execute_command(cmd)
+ return (rc, out, err)
+ return (None, '', '')
+
+ def get_lowest_available_system_gid(self):
+ # check for lowest available system gid (< 500)
+ try:
+ cmd = [self.module.get_bin_path('dscl', True)]
+ cmd += ['/Local/Default', '-list', '/Groups', 'PrimaryGroupID']
+ (rc, out, err) = self.execute_command(cmd)
+ lines = out.splitlines()
+ highest = 0
+ for group_info in lines:
+ parts = group_info.split(' ')
+ if len(parts) > 1:
+ gid = int(parts[-1])
+ if gid > highest and gid < 500:
+ highest = gid
+ if highest == 0 or highest == 499:
+ return False
+ return (highest + 1)
+ except Exception:
+ return False
+
+
+class OpenBsdGroup(Group):
+ """
+ This is a OpenBSD Group manipulation class.
+
+ This overrides the following methods from the generic class:-
+ - group_del()
+ - group_add()
+ - group_mod()
+ """
+
+ platform = 'OpenBSD'
+ distribution = None
+ GROUPFILE = '/etc/group'
+
+ def group_del(self):
+ cmd = [self.module.get_bin_path('groupdel', True), self.name]
+ return self.execute_command(cmd)
+
+ def group_add(self, **kwargs):
+ cmd = [self.module.get_bin_path('groupadd', True)]
+ if self.gid is not None:
+ cmd.append('-g')
+ cmd.append(str(self.gid))
+ if self.non_unique:
+ cmd.append('-o')
+ cmd.append(self.name)
+ return self.execute_command(cmd)
+
+ def group_mod(self, **kwargs):
+ cmd = [self.module.get_bin_path('groupmod', True)]
+ info = self.group_info()
+ if self.gid is not None and int(self.gid) != info[2]:
+ cmd.append('-g')
+ cmd.append(str(self.gid))
+ if self.non_unique:
+ cmd.append('-o')
+ if len(cmd) == 1:
+ return (None, '', '')
+ if self.module.check_mode:
+ return (0, '', '')
+ cmd.append(self.name)
+ return self.execute_command(cmd)
+
+
+# ===========================================
+
+class NetBsdGroup(Group):
+ """
+ This is a NetBSD Group manipulation class.
+
+ This overrides the following methods from the generic class:-
+ - group_del()
+ - group_add()
+ - group_mod()
+ """
+
+ platform = 'NetBSD'
+ distribution = None
+ GROUPFILE = '/etc/group'
+
+ def group_del(self):
+ cmd = [self.module.get_bin_path('groupdel', True), self.name]
+ return self.execute_command(cmd)
+
+ def group_add(self, **kwargs):
+ cmd = [self.module.get_bin_path('groupadd', True)]
+ if self.gid is not None:
+ cmd.append('-g')
+ cmd.append(str(self.gid))
+ if self.non_unique:
+ cmd.append('-o')
+ cmd.append(self.name)
+ return self.execute_command(cmd)
+
+ def group_mod(self, **kwargs):
+ cmd = [self.module.get_bin_path('groupmod', True)]
+ info = self.group_info()
+ if self.gid is not None and int(self.gid) != info[2]:
+ cmd.append('-g')
+ cmd.append(str(self.gid))
+ if self.non_unique:
+ cmd.append('-o')
+ if len(cmd) == 1:
+ return (None, '', '')
+ if self.module.check_mode:
+ return (0, '', '')
+ cmd.append(self.name)
+ return self.execute_command(cmd)
+
+
+# ===========================================
+
+
+class BusyBoxGroup(Group):
+ """
+ BusyBox group manipulation class for systems that have addgroup and delgroup.
+
+ It overrides the following methods:
+ - group_add()
+ - group_del()
+ - group_mod()
+ """
+
+ def group_add(self, **kwargs):
+ cmd = [self.module.get_bin_path('addgroup', True)]
+ if self.gid is not None:
+ cmd.extend(['-g', str(self.gid)])
+
+ if self.system:
+ cmd.append('-S')
+
+ cmd.append(self.name)
+
+ return self.execute_command(cmd)
+
+ def group_del(self):
+ cmd = [self.module.get_bin_path('delgroup', True), self.name]
+ return self.execute_command(cmd)
+
+ def group_mod(self, **kwargs):
+ # Since there is no groupmod command, modify /etc/group directly
+ info = self.group_info()
+ if self.gid is not None and self.gid != info[2]:
+ with open('/etc/group', 'rb') as f:
+ b_groups = f.read()
+
+ b_name = to_bytes(self.name)
+ b_current_group_string = b'%s:x:%d:' % (b_name, info[2])
+ b_new_group_string = b'%s:x:%d:' % (b_name, self.gid)
+
+ if b':%d:' % self.gid in b_groups:
+ self.module.fail_json(msg="gid '{gid}' in use".format(gid=self.gid))
+
+ if self.module.check_mode:
+ return 0, '', ''
+ b_new_groups = b_groups.replace(b_current_group_string, b_new_group_string)
+ with open('/etc/group', 'wb') as f:
+ f.write(b_new_groups)
+ return 0, '', ''
+
+ return None, '', ''
+
+
+class AlpineGroup(BusyBoxGroup):
+
+ platform = 'Linux'
+ distribution = 'Alpine'
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(type='str', default='present', choices=['absent', 'present']),
+ name=dict(type='str', required=True),
+ gid=dict(type='int'),
+ system=dict(type='bool', default=False),
+ local=dict(type='bool', default=False),
+ non_unique=dict(type='bool', default=False),
+ ),
+ supports_check_mode=True,
+ required_if=[
+ ['non_unique', True, ['gid']],
+ ],
+ )
+
+ group = Group(module)
+
+ module.debug('Group instantiated - platform %s' % group.platform)
+ if group.distribution:
+ module.debug('Group instantiated - distribution %s' % group.distribution)
+
+ rc = None
+ out = ''
+ err = ''
+ result = {}
+ result['name'] = group.name
+ result['state'] = group.state
+
+ if group.state == 'absent':
+
+ if group.group_exists():
+ if module.check_mode:
+ module.exit_json(changed=True)
+ (rc, out, err) = group.group_del()
+ if rc != 0:
+ module.fail_json(name=group.name, msg=err)
+
+ elif group.state == 'present':
+
+ if not group.group_exists():
+ if module.check_mode:
+ module.exit_json(changed=True)
+ (rc, out, err) = group.group_add(gid=group.gid, system=group.system)
+ else:
+ (rc, out, err) = group.group_mod(gid=group.gid)
+
+ if rc is not None and rc != 0:
+ module.fail_json(name=group.name, msg=err)
+
+ if rc is None:
+ result['changed'] = False
+ else:
+ result['changed'] = True
+ if out:
+ result['stdout'] = out
+ if err:
+ result['stderr'] = err
+
+ if group.group_exists():
+ info = group.group_info()
+ result['system'] = group.system
+ result['gid'] = info[2]
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()