mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
New Module: Homectl module for managing systemd-homed (#4018)
* initial development of homectl module * botmeta * fix some linting * Update .github/BOTMETA.yml Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/system/homectl.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/system/homectl.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/system/homectl.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/system/homectl.py Co-authored-by: Felix Fontein <felix@fontein.de> * use array form of run_command Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/system/homectl.py Co-authored-by: Felix Fontein <felix@fontein.de> * added mofifying user record and cleaned up based on comments * added updating records/multiple changes regarding options, examples doc, return doc * add integration tests and more overall improvements * Update plugins/modules/system/homectl.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/system/homectl.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/system/homectl.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/system/homectl.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/system/homectl.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/system/homectl.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/system/homectl.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/system/homectl.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/system/homectl.py Co-authored-by: Felix Fontein <felix@fontein.de> * removed modify handle within present * adding more options and better checking of user records when updating * Apply suggestions from code review Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/system/homectl.py Co-authored-by: Felix Fontein <felix@fontein.de> * Add code review changes - remove unsafe_shell with run_command. - use dict.pop() in user_metadata dict. - consistent quoting to single quotes. - change logic to determine check mode better - fix integration tests and added check_mode tests * Fix handling of mount opts When a user is created without mountopts homed will use nodev and nosuid by default, however the user record metadata will not contain these values. This commit takes extra care that correct value is being set to true or false. So if a user gives mountopts with just nodev we need to make sure the nosuid and noexec gets set to false, etc. If mountopts are same as currently in user record make sure nothing would be changed and outputs correctly. Also fixed some tests. * change fmethod modify_user to prepare_modify_user_command * Code review fixes and add existing user pw checking - Added methods to check existing users password is correct by comparing the hash stored in homed user record and the hash of given password - Updated integration tests for above case - Added aliases file so CI can run * Apply suggestions from code review Co-authored-by: Felix Fontein <felix@fontein.de> Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
parent
7aab4497ac
commit
ab7e4ee578
5 changed files with 832 additions and 0 deletions
2
.github/BOTMETA.yml
vendored
2
.github/BOTMETA.yml
vendored
|
@ -1022,6 +1022,8 @@ files:
|
|||
$modules/system/gconftool2.py:
|
||||
maintainers: Akasurde kevensen
|
||||
labels: gconftool2
|
||||
$modules/system/homectl.py:
|
||||
maintainers: jameslivulpi
|
||||
$modules/system/interfaces_file.py:
|
||||
maintainers: obourdon hryamzik
|
||||
labels: interfaces_file
|
||||
|
|
1
plugins/modules/homectl.py
Symbolic link
1
plugins/modules/homectl.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
./system/homectl.py
|
650
plugins/modules/system/homectl.py
Normal file
650
plugins/modules/system/homectl.py
Normal file
|
@ -0,0 +1,650 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2022, James Livulpi
|
||||
# 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: homectl
|
||||
author:
|
||||
- "James Livulpi (@jameslivulpi)"
|
||||
short_description: Manage user accounts with systemd-homed
|
||||
version_added: 4.4.0
|
||||
description:
|
||||
- Manages a user's home directory managed by systemd-homed.
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- The user name to create, remove, or update.
|
||||
required: true
|
||||
aliases: [ 'user', 'username' ]
|
||||
type: str
|
||||
password:
|
||||
description:
|
||||
- Set the user's password to this.
|
||||
- Homed requires this value to be in cleartext on user creation and updating a user.
|
||||
- The module takes the password and generates a password hash in SHA-512 with 10000 rounds of salt generation using crypt.
|
||||
- See U(https://systemd.io/USER_RECORD/).
|
||||
- This is required for I(state=present). When an existing user is updated this is checked against the stored hash in homed.
|
||||
type: str
|
||||
state:
|
||||
description:
|
||||
- The operation to take on the user.
|
||||
choices: [ 'absent', 'present' ]
|
||||
default: present
|
||||
type: str
|
||||
storage:
|
||||
description:
|
||||
- Indicates the storage mechanism for the user's home directory.
|
||||
- If the storage type is not specified, ``homed.conf(5)`` defines which default storage to use.
|
||||
- Only used when a user is first created.
|
||||
choices: [ 'classic', 'luks', 'directory', 'subvolume', 'fscrypt', 'cifs' ]
|
||||
type: str
|
||||
disksize:
|
||||
description:
|
||||
- The intended home directory disk space.
|
||||
- Human readable value such as C(10G), C(10M), or C(10B).
|
||||
type: str
|
||||
resize:
|
||||
description:
|
||||
- When used with I(disksize) this will attempt to resize the home directory immediately.
|
||||
default: false
|
||||
type: bool
|
||||
realname:
|
||||
description:
|
||||
- The user's real ('human') name.
|
||||
- This can also be used to add a comment to maintain compatability with C(useradd).
|
||||
aliases: [ 'comment' ]
|
||||
type: str
|
||||
realm:
|
||||
description:
|
||||
- The 'realm' a user is defined in.
|
||||
type: str
|
||||
email:
|
||||
description:
|
||||
- The email address of the user.
|
||||
type: str
|
||||
location:
|
||||
description:
|
||||
- A free-form location string describing the location of the user.
|
||||
type: str
|
||||
iconname:
|
||||
description:
|
||||
- The name of an icon picked by the user, for example for the purpose of an avatar.
|
||||
- Should follow the semantics defined in the Icon Naming Specification.
|
||||
- See U(https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html) for specifics.
|
||||
type: str
|
||||
homedir:
|
||||
description:
|
||||
- Path to use as home directory for the user.
|
||||
- This is the directory the user's home directory is mounted to while the user is logged in.
|
||||
- This is not where the user's data is actually stored, see I(imagepath) for that.
|
||||
- Only used when a user is first created.
|
||||
type: path
|
||||
imagepath:
|
||||
description:
|
||||
- Path to place the user's home directory.
|
||||
- See U(https://www.freedesktop.org/software/systemd/man/homectl.html#--image-path=PATH) for more information.
|
||||
- Only used when a user is first created.
|
||||
type: path
|
||||
uid:
|
||||
description:
|
||||
- Sets the UID of the user.
|
||||
- If using I(gid) homed requires the value to be the same.
|
||||
- Only used when a user is first created.
|
||||
type: int
|
||||
gid:
|
||||
description:
|
||||
- Sets the gid of the user.
|
||||
- If using I(uid) homed requires the value to be the same.
|
||||
- Only used when a user is first created.
|
||||
type: int
|
||||
mountopts:
|
||||
description:
|
||||
- String separated by comma each indicating mount options for a users home directory.
|
||||
- Valid options are C(nosuid), C(nodev) or C(noexec).
|
||||
- Homed by default uses C(nodev) and C(nosuid) while C(noexec) is off.
|
||||
type: str
|
||||
umask:
|
||||
description:
|
||||
- Sets the umask for the user's login sessions
|
||||
- Value from C(0000) to C(0777).
|
||||
type: int
|
||||
memberof:
|
||||
description:
|
||||
- String separated by comma each indicating a UNIX group this user shall be a member of.
|
||||
- Groups the user should be a member of should be supplied as comma separated list.
|
||||
aliases: [ 'groups' ]
|
||||
type: str
|
||||
skeleton:
|
||||
description:
|
||||
- The absolute path to the skeleton directory to populate a new home directory from.
|
||||
- This is only used when a home directory is first created.
|
||||
- If not specified homed by default uses C(/etc/skel).
|
||||
aliases: [ 'skel' ]
|
||||
type: path
|
||||
shell:
|
||||
description:
|
||||
- Shell binary to use for terminal logins of given user.
|
||||
- If not specified homed by default uses C(/bin/bash).
|
||||
type: str
|
||||
environment:
|
||||
description:
|
||||
- String separated by comma each containing an environment variable and its value to
|
||||
set for the user's login session, in a format compatible with ``putenv()``.
|
||||
- Any environment variable listed here is automatically set by pam_systemd for all
|
||||
login sessions of the user.
|
||||
aliases: [ 'setenv' ]
|
||||
type: str
|
||||
timezone:
|
||||
description:
|
||||
- Preferred timezone to use for the user.
|
||||
- Should be a tzdata compatible location string such as C(America/New_York).
|
||||
type: str
|
||||
locked:
|
||||
description:
|
||||
- Whether the user account should be locked or not.
|
||||
type: bool
|
||||
language:
|
||||
description:
|
||||
- The preferred language/locale for the user.
|
||||
- This should be in a format compatible with the C($LANG) environment variable.
|
||||
type: str
|
||||
passwordhint:
|
||||
description:
|
||||
- Password hint for the given user.
|
||||
type: str
|
||||
sshkeys:
|
||||
description:
|
||||
- String separated by comma each listing a SSH public key that is authorized to access the account.
|
||||
- The keys should follow the same format as the lines in a traditional C(~/.ssh/authorized_key) file.
|
||||
type: str
|
||||
notbefore:
|
||||
description:
|
||||
- A time since the UNIX epoch before which the record should be considered invalid for the purpose of logging in.
|
||||
type: int
|
||||
notafter:
|
||||
description:
|
||||
- A time since the UNIX epoch after which the record should be considered invalid for the purpose of logging in.
|
||||
type: int
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Add the user 'james'
|
||||
community.general.homectl:
|
||||
name: johnd
|
||||
password: myreallysecurepassword1!
|
||||
state: present
|
||||
|
||||
- name: Add the user 'alice' with a zsh shell, uid of 1000, and gid of 2000
|
||||
community.general.homectl:
|
||||
name: alice
|
||||
password: myreallysecurepassword1!
|
||||
state: present
|
||||
shell: /bin/zsh
|
||||
uid: 1000
|
||||
gid: 1000
|
||||
|
||||
- name: Modify an existing user 'frank' to have 10G of diskspace and resize usage now
|
||||
community.general.homectl:
|
||||
name: frank
|
||||
password: myreallysecurepassword1!
|
||||
state: present
|
||||
disksize: 10G
|
||||
resize: yes
|
||||
|
||||
- name: Remove an existing user 'janet'
|
||||
community.general.homectl:
|
||||
name: janet
|
||||
state: absent
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
data:
|
||||
description: A json dictionary returned from C(homectl inspect -j).
|
||||
returned: success
|
||||
type: dict
|
||||
sample: {
|
||||
"data": {
|
||||
"binding": {
|
||||
"e9ed2a5b0033427286b228e97c1e8343": {
|
||||
"fileSystemType": "btrfs",
|
||||
"fileSystemUuid": "7bd59491-2812-4642-a492-220c3f0c6c0b",
|
||||
"gid": 60268,
|
||||
"imagePath": "/home/james.home",
|
||||
"luksCipher": "aes",
|
||||
"luksCipherMode": "xts-plain64",
|
||||
"luksUuid": "7f05825a-2c38-47b4-90e1-f21540a35a81",
|
||||
"luksVolumeKeySize": 32,
|
||||
"partitionUuid": "5a906126-d3c8-4234-b230-8f6e9b427b2f",
|
||||
"storage": "luks",
|
||||
"uid": 60268
|
||||
}
|
||||
},
|
||||
"diskSize": 3221225472,
|
||||
"disposition": "regular",
|
||||
"lastChangeUSec": 1641941238208691,
|
||||
"lastPasswordChangeUSec": 1641941238208691,
|
||||
"privileged": {
|
||||
"hashedPassword": [
|
||||
"$6$ov9AKni.trf76inT$tTtfSyHgbPTdUsG0CvSSQZXGqFGdHKQ9Pb6e0BTZhDmlgrL/vA5BxrXduBi8u/PCBiYUffGLIkGhApjKMK3bV."
|
||||
]
|
||||
},
|
||||
"signature": [
|
||||
{
|
||||
"data": "o6zVFbymcmk4YTVaY6KPQK23YCp+VkXdGEeniZeV1pzIbFzoaZBvVLPkNKMoPAQbodY5BYfBtuy41prNL78qAg==",
|
||||
"key": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAbs7ELeiEYBxkUQhxZ+5NGyu6J7gTtZtZ5vmIw3jowcY=\n-----END PUBLIC KEY-----\n"
|
||||
}
|
||||
],
|
||||
"status": {
|
||||
"e9ed2a5b0033427286b228e97c1e8343": {
|
||||
"diskCeiling": 21845405696,
|
||||
"diskFloor": 268435456,
|
||||
"diskSize": 3221225472,
|
||||
"service": "io.systemd.Home",
|
||||
"signedLocally": true,
|
||||
"state": "inactive"
|
||||
}
|
||||
},
|
||||
"userName": "james",
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
import crypt
|
||||
import json
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.basic import jsonify
|
||||
from ansible.module_utils.common.text.formatters import human_to_bytes
|
||||
|
||||
|
||||
class Homectl(object):
|
||||
'''#TODO DOC STRINGS'''
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
self.state = module.params['state']
|
||||
self.name = module.params['name']
|
||||
self.password = module.params['password']
|
||||
self.storage = module.params['storage']
|
||||
self.disksize = module.params['disksize']
|
||||
self.resize = module.params['resize']
|
||||
self.realname = module.params['realname']
|
||||
self.realm = module.params['realm']
|
||||
self.email = module.params['email']
|
||||
self.location = module.params['location']
|
||||
self.iconname = module.params['iconname']
|
||||
self.homedir = module.params['homedir']
|
||||
self.imagepath = module.params['imagepath']
|
||||
self.uid = module.params['uid']
|
||||
self.gid = module.params['gid']
|
||||
self.umask = module.params['umask']
|
||||
self.memberof = module.params['memberof']
|
||||
self.skeleton = module.params['skeleton']
|
||||
self.shell = module.params['shell']
|
||||
self.environment = module.params['environment']
|
||||
self.timezone = module.params['timezone']
|
||||
self.locked = module.params['locked']
|
||||
self.passwordhint = module.params['passwordhint']
|
||||
self.sshkeys = module.params['sshkeys']
|
||||
self.language = module.params['language']
|
||||
self.notbefore = module.params['notbefore']
|
||||
self.notafter = module.params['notafter']
|
||||
self.mountopts = module.params['mountopts']
|
||||
|
||||
self.result = {}
|
||||
|
||||
# Cannot run homectl commands if service is not active
|
||||
def homed_service_active(self):
|
||||
is_active = True
|
||||
cmd = ['systemctl', 'show', 'systemd-homed.service', '-p', 'ActiveState']
|
||||
rc, show_service_stdout, stderr = self.module.run_command(cmd)
|
||||
if rc == 0:
|
||||
state = show_service_stdout.rsplit('=')[1]
|
||||
if state.strip() != 'active':
|
||||
is_active = False
|
||||
return is_active
|
||||
|
||||
def user_exists(self):
|
||||
exists = False
|
||||
valid_pw = False
|
||||
# Get user properties if they exist in json
|
||||
rc, stdout, stderr = self.get_user_metadata()
|
||||
if rc == 0:
|
||||
exists = True
|
||||
# User exists now compare password given with current hashed password stored in the user metadata.
|
||||
if self.state != 'absent': # Don't need checking on remove user
|
||||
stored_pwhash = json.loads(stdout)['privileged']['hashedPassword'][0]
|
||||
if self._check_password(stored_pwhash):
|
||||
valid_pw = True
|
||||
return exists, valid_pw
|
||||
|
||||
def create_user(self):
|
||||
record = self.create_json_record(create=True)
|
||||
cmd = [self.module.get_bin_path('homectl', True)]
|
||||
cmd.append('create')
|
||||
cmd.append('--identity=-') # Read the user record from standard input.
|
||||
return(self.module.run_command(cmd, data=record))
|
||||
|
||||
def _hash_password(self, password):
|
||||
method = crypt.METHOD_SHA512
|
||||
salt = crypt.mksalt(method, rounds=10000)
|
||||
pw_hash = crypt.crypt(password, salt)
|
||||
return pw_hash
|
||||
|
||||
def _check_password(self, pwhash):
|
||||
hash = crypt.crypt(self.password, pwhash)
|
||||
return pwhash == hash
|
||||
|
||||
def remove_user(self):
|
||||
cmd = [self.module.get_bin_path('homectl', True)]
|
||||
cmd.append('remove')
|
||||
cmd.append(self.name)
|
||||
return self.module.run_command(cmd)
|
||||
|
||||
def prepare_modify_user_command(self):
|
||||
record = self.create_json_record()
|
||||
cmd = [self.module.get_bin_path('homectl', True)]
|
||||
cmd.append('update')
|
||||
cmd.append(self.name)
|
||||
cmd.append('--identity=-') # Read the user record from standard input.
|
||||
# Resize disksize now resize = true
|
||||
# This is not valid in user record (json) and requires it to be passed on command.
|
||||
if self.disksize and self.resize:
|
||||
cmd.append('--and-resize')
|
||||
cmd.append('true')
|
||||
self.result['changed'] = True
|
||||
return cmd, record
|
||||
|
||||
def get_user_metadata(self):
|
||||
cmd = [self.module.get_bin_path('homectl', True)]
|
||||
cmd.append('inspect')
|
||||
cmd.append(self.name)
|
||||
cmd.append('-j')
|
||||
cmd.append('--no-pager')
|
||||
rc, stdout, stderr = self.module.run_command(cmd)
|
||||
return rc, stdout, stderr
|
||||
|
||||
# Build up dictionary to jsonify for homectl commands.
|
||||
def create_json_record(self, create=False):
|
||||
record = {}
|
||||
user_metadata = {}
|
||||
self.result['changed'] = False
|
||||
# Get the current user record if not creating a new user record.
|
||||
if not create:
|
||||
rc, user_metadata, stderr = self.get_user_metadata()
|
||||
user_metadata = json.loads(user_metadata)
|
||||
# Remove elements that are not meant to be updated from record.
|
||||
# These are always part of the record when a user exists.
|
||||
user_metadata.pop('signature', None)
|
||||
user_metadata.pop('binding', None)
|
||||
user_metadata.pop('status', None)
|
||||
# Let last change Usec be updated by homed when command runs.
|
||||
user_metadata.pop('lastChangeUSec', None)
|
||||
# Now only change fields that are called on leaving whats currently in the record intact.
|
||||
record = user_metadata
|
||||
|
||||
record['userName'] = self.name
|
||||
record['secret'] = {'password': [self.password]}
|
||||
|
||||
if create:
|
||||
password_hash = self._hash_password(self.password)
|
||||
record['privileged'] = {'hashedPassword': [password_hash]}
|
||||
self.result['changed'] = True
|
||||
|
||||
if self.uid and self.gid and create:
|
||||
record['uid'] = self.uid
|
||||
record['gid'] = self.gid
|
||||
self.result['changed'] = True
|
||||
|
||||
if self.memberof:
|
||||
member_list = list(self.memberof.split(','))
|
||||
if member_list != record.get('memberOf', [None]):
|
||||
record['memberOf'] = member_list
|
||||
self.result['changed'] = True
|
||||
|
||||
if self.realname:
|
||||
if self.realname != record.get('realName'):
|
||||
record['realName'] = self.realname
|
||||
self.result['changed'] = True
|
||||
|
||||
# Cannot update storage unless were creating a new user.
|
||||
# See 'Fields in the binding section' at https://systemd.io/USER_RECORD/
|
||||
if self.storage and create:
|
||||
record['storage'] = self.storage
|
||||
self.result['changed'] = True
|
||||
|
||||
# Cannot update homedir unless were creating a new user.
|
||||
# See 'Fields in the binding section' at https://systemd.io/USER_RECORD/
|
||||
if self.homedir and create:
|
||||
record['homeDirectory'] = self.homedir
|
||||
self.result['changed'] = True
|
||||
|
||||
# Cannot update imagepath unless were creating a new user.
|
||||
# See 'Fields in the binding section' at https://systemd.io/USER_RECORD/
|
||||
if self.imagepath and create:
|
||||
record['imagePath'] = self.imagepath
|
||||
self.result['changed'] = True
|
||||
|
||||
if self.disksize:
|
||||
# convert humand readble to bytes
|
||||
if self.disksize != record.get('diskSize'):
|
||||
record['diskSize'] = human_to_bytes(self.disksize)
|
||||
self.result['changed'] = True
|
||||
|
||||
if self.realm:
|
||||
if self.realm != record.get('realm'):
|
||||
record['realm'] = self.realm
|
||||
self.result['changed'] = True
|
||||
|
||||
if self.email:
|
||||
if self.email != record.get('emailAddress'):
|
||||
record['emailAddress'] = self.email
|
||||
self.result['changed'] = True
|
||||
|
||||
if self.location:
|
||||
if self.location != record.get('location'):
|
||||
record['location'] = self.location
|
||||
self.result['changed'] = True
|
||||
|
||||
if self.iconname:
|
||||
if self.iconname != record.get('iconName'):
|
||||
record['iconName'] = self.iconname
|
||||
self.result['changed'] = True
|
||||
|
||||
if self.skeleton:
|
||||
if self.skeleton != record.get('skeletonDirectory'):
|
||||
record['skeletonDirectory'] = self.skeleton
|
||||
self.result['changed'] = True
|
||||
|
||||
if self.shell:
|
||||
if self.shell != record.get('shell'):
|
||||
record['shell'] = self.shell
|
||||
self.result['changed'] = True
|
||||
|
||||
if self.umask:
|
||||
if self.umask != record.get('umask'):
|
||||
record['umask'] = self.umask
|
||||
self.result['changed'] = True
|
||||
|
||||
if self.environment:
|
||||
if self.environment != record.get('environment', [None]):
|
||||
record['environment'] = list(self.environment.split(','))
|
||||
self.result['changed'] = True
|
||||
|
||||
if self.timezone:
|
||||
if self.timezone != record.get('timeZone'):
|
||||
record['timeZone'] = self.timezone
|
||||
self.result['changed'] = True
|
||||
|
||||
if self.locked:
|
||||
if self.locked != record.get('locked'):
|
||||
record['locked'] = self.locked
|
||||
self.result['changed'] = True
|
||||
|
||||
if self.passwordhint:
|
||||
if self.passwordhint != record.get('privileged', {}).get('passwordHint'):
|
||||
record['privileged']['passwordHint'] = self.passwordhint
|
||||
self.result['changed'] = True
|
||||
|
||||
if self.sshkeys:
|
||||
if self.sshkeys != record.get('privileged', {}).get('sshAuthorizedKeys'):
|
||||
record['privileged']['sshAuthorizedKeys'] = list(self.sshkeys.split(','))
|
||||
self.result['changed'] = True
|
||||
|
||||
if self.language:
|
||||
if self.locked != record.get('preferredLanguage'):
|
||||
record['preferredLanguage'] = self.language
|
||||
self.result['changed'] = True
|
||||
|
||||
if self.notbefore:
|
||||
if self.locked != record.get('notBeforeUSec'):
|
||||
record['notBeforeUSec'] = self.notbefore
|
||||
self.result['changed'] = True
|
||||
|
||||
if self.notafter:
|
||||
if self.locked != record.get('notAfterUSec'):
|
||||
record['notAfterUSec'] = self.notafter
|
||||
self.result['changed'] = True
|
||||
|
||||
if self.mountopts:
|
||||
opts = list(self.mountopts.split(','))
|
||||
if 'nosuid' in opts:
|
||||
if record.get('mountNoSuid') is not True:
|
||||
record['mountNoSuid'] = True
|
||||
self.result['changed'] = True
|
||||
else:
|
||||
if record.get('mountNoSuid') is not False:
|
||||
record['mountNoSuid'] = False
|
||||
self.result['changed'] = True
|
||||
|
||||
if 'nodev' in opts:
|
||||
if record.get('mountNoDevices') is not True:
|
||||
record['mountNoDevices'] = True
|
||||
self.result['changed'] = True
|
||||
else:
|
||||
if record.get('mountNoDevices') is not False:
|
||||
record['mountNoDevices'] = False
|
||||
self.result['changed'] = True
|
||||
|
||||
if 'noexec' in opts:
|
||||
if record.get('mountNoExecute') is not True:
|
||||
record['mountNoExecute'] = True
|
||||
self.result['changed'] = True
|
||||
else:
|
||||
if record.get('mountNoExecute') is not False:
|
||||
record['mountNoExecute'] = False
|
||||
self.result['changed'] = True
|
||||
|
||||
return jsonify(record)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
state=dict(type='str', default='present', choices=['absent', 'present']),
|
||||
name=dict(type='str', required=True, aliases=['user', 'username']),
|
||||
password=dict(type='str', no_log=True),
|
||||
storage=dict(type='str', choices=['classic', 'luks', 'directory', 'subvolume', 'fscrypt', 'cifs']),
|
||||
disksize=dict(type='str'),
|
||||
resize=dict(type='bool', default=False),
|
||||
realname=dict(type='str', aliases=['comment']),
|
||||
realm=dict(type='str'),
|
||||
email=dict(type='str'),
|
||||
location=dict(type='str'),
|
||||
iconname=dict(type='str'),
|
||||
homedir=dict(type='path'),
|
||||
imagepath=dict(type='path'),
|
||||
uid=dict(type='int'),
|
||||
gid=dict(type='int'),
|
||||
umask=dict(type='int'),
|
||||
environment=dict(type='str', aliases=['setenv']),
|
||||
timezone=dict(type='str'),
|
||||
memberof=dict(type='str', aliases=['groups']),
|
||||
skeleton=dict(type='path', aliases=['skel']),
|
||||
shell=dict(type='str'),
|
||||
locked=dict(type='bool'),
|
||||
passwordhint=dict(type='str', no_log=True),
|
||||
sshkeys=dict(type='str', no_log=True),
|
||||
language=dict(type='str'),
|
||||
notbefore=dict(type='int'),
|
||||
notafter=dict(type='int'),
|
||||
mountopts=dict(type='str'),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
|
||||
required_if=[
|
||||
('state', 'present', ['password']),
|
||||
('resize', True, ['disksize']),
|
||||
]
|
||||
)
|
||||
|
||||
homectl = Homectl(module)
|
||||
homectl.result['state'] = homectl.state
|
||||
|
||||
# First we need to make sure homed service is active
|
||||
if not homectl.homed_service_active():
|
||||
module.fail_json(msg='systemd-homed.service is not active')
|
||||
|
||||
# handle removing user
|
||||
if homectl.state == 'absent':
|
||||
user_exists, valid_pwhash = homectl.user_exists()
|
||||
if user_exists:
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
rc, stdout, stderr = homectl.remove_user()
|
||||
if rc != 0:
|
||||
module.fail_json(name=homectl.name, msg=stderr, rc=rc)
|
||||
homectl.result['changed'] = True
|
||||
homectl.result['rc'] = rc
|
||||
homectl.result['msg'] = 'User %s removed!' % homectl.name
|
||||
else:
|
||||
homectl.result['changed'] = False
|
||||
homectl.result['msg'] = 'User does not exist!'
|
||||
|
||||
# Handle adding a user
|
||||
if homectl.state == 'present':
|
||||
user_exists, valid_pwhash = homectl.user_exists()
|
||||
if not user_exists:
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
rc, stdout, stderr = homectl.create_user()
|
||||
if rc != 0:
|
||||
module.fail_json(name=homectl.name, msg=stderr, rc=rc)
|
||||
rc, user_metadata, stderr = homectl.get_user_metadata()
|
||||
homectl.result['data'] = json.loads(user_metadata)
|
||||
homectl.result['rc'] = rc
|
||||
homectl.result['msg'] = 'User %s created!' % homectl.name
|
||||
else:
|
||||
if valid_pwhash:
|
||||
# Run this to see if changed would be True or False which is useful for check_mode
|
||||
cmd, record = homectl.prepare_modify_user_command()
|
||||
else:
|
||||
# User gave wrong password fail with message
|
||||
homectl.result['changed'] = False
|
||||
homectl.result['msg'] = 'User exists but password is incorrect!'
|
||||
module.fail_json(**homectl.result)
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**homectl.result)
|
||||
|
||||
# Now actually modify the user if changed was set to true at any point.
|
||||
if homectl.result['changed']:
|
||||
rc, stdout, stderr = module.run_command(cmd, data=record)
|
||||
if rc != 0:
|
||||
module.fail_json(name=homectl.name, msg=stderr, rc=rc, changed=False)
|
||||
rc, user_metadata, stderr = homectl.get_user_metadata()
|
||||
homectl.result['data'] = json.loads(user_metadata)
|
||||
homectl.result['rc'] = rc
|
||||
if homectl.result['changed']:
|
||||
homectl.result['msg'] = 'User %s modified' % homectl.name
|
||||
|
||||
module.exit_json(**homectl.result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
5
tests/integration/targets/homectl/aliases
Normal file
5
tests/integration/targets/homectl/aliases
Normal file
|
@ -0,0 +1,5 @@
|
|||
shippable/posix/group1
|
||||
skip/aix
|
||||
skip/freebsd
|
||||
skip/osx
|
||||
skip/macos
|
174
tests/integration/targets/homectl/tasks/main.yml
Normal file
174
tests/integration/targets/homectl/tasks/main.yml
Normal file
|
@ -0,0 +1,174 @@
|
|||
# Get systemd version and if it doesn't exist don't run these tests.
|
||||
- name: check systemd version
|
||||
command: "systemctl --version"
|
||||
register: systemd_version
|
||||
ignore_errors: yes
|
||||
|
||||
- name: check homectl version
|
||||
command: homectl --version
|
||||
register: homectl_version
|
||||
ignore_errors: yes
|
||||
|
||||
- block:
|
||||
- name: Check and start systemd-homed service
|
||||
service:
|
||||
name: systemd-homed.service
|
||||
state: started
|
||||
enabled: yes
|
||||
|
||||
- name: Add a user 'james'
|
||||
community.general.homectl:
|
||||
name: james
|
||||
password: myreallysecurepassword1!
|
||||
state: present
|
||||
|
||||
- name: verify user added
|
||||
command: homectl inspect james
|
||||
register: james_info
|
||||
|
||||
- name: Add the user 'tom' with a zsh shell, uid of 1000, and gid of 1000
|
||||
community.general.homectl:
|
||||
name: tom
|
||||
password: myreallysecurepassword1!
|
||||
state: present
|
||||
shell: /bin/zsh
|
||||
uid: 1000
|
||||
gid: 1000
|
||||
disksize: 10G
|
||||
register: tom_userinfo
|
||||
|
||||
- name: Try to add user 'james' that already exists
|
||||
community.general.homectl:
|
||||
name: james
|
||||
password: myreallysecurepassword1!
|
||||
state: present
|
||||
shell: /bin/ksh
|
||||
register: user_exists
|
||||
|
||||
- name: Try to use 'resize=yes' option without 'disksize' option (not allowed)
|
||||
community.general.homectl:
|
||||
name: foo
|
||||
password: uq4895738!@#$%dfd
|
||||
state: present
|
||||
resize: yes
|
||||
register: resize_out
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Use option 'disksize=1G' without option resize (allowed)
|
||||
community.general.homectl:
|
||||
name: foobar
|
||||
password: "uq4895738!@#$%dfd"
|
||||
state: present
|
||||
disksize: 1G
|
||||
register: disk_out
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Try to Create user without giving password
|
||||
community.general.homectl:
|
||||
name: danielle
|
||||
register: danielle_out
|
||||
ignore_errors: yes
|
||||
|
||||
- name: remove user 'foobar' without requiring password
|
||||
community.general.homectl:
|
||||
name: foobar
|
||||
state: absent
|
||||
register: delete_foobar_out
|
||||
|
||||
- name: modify user 'james' to have zsh shell and timezone 'America/New_York'
|
||||
community.general.homectl:
|
||||
name: james
|
||||
password: myreallysecurepassword1!
|
||||
state: present
|
||||
shell: /bin/zsh
|
||||
timezone: America/New_York
|
||||
register: lukuser_modify_out
|
||||
|
||||
- name: create user 'jake' with all mount options
|
||||
community.general.homectl:
|
||||
name: jake
|
||||
password: myreallysecurepassword12!
|
||||
mountopts: noexec,nosuid,nodev
|
||||
sshkeys: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDUSW/q2qFZPr2vS0qrmAs+1iQI1jLIBdJ4CVIhE3KnIwxkkiInS8mJ+t0FBTeK3ks3GZLPVYC1v9o2P+oqyUk1CiBnCsMXGJud+L/t8b5r8MiJMyP7Jzd6yhmcvenjvz+vY06jQ9chWAtThEknuaOMongIpQQzSLGbdMy0yMsz4GEjicwdcj1PDwItPvUt4TL4K7V9NE672idADlRt6qng4UwpziqlYgsyIG46ettDras8hGAPricrhFWUS2rLDsCD0thkPFdR8HL1ZWTZ6LtolhO4MYtgntzXn708TTmFC2oIDluzyxVoUYmsfVotVdXFZcOWffnwbCgU+tn75JXTLozgTbV3VWmkxpJFErCWPerxcZv3+7b0f36/Y0gRNjM9HERLDSE1c8yz29NOLY0qH5306aByjOaerxNq9+ZOU/Fmf5/VfGIUp/FdLxDw+V0AzejFG580VAcstEMsOHSdwTbi3gf6LoGSiRyWKKDod0TZCMC6RzfdsfdsfI9CClGl0s= test@router.home"
|
||||
register: jake_out
|
||||
|
||||
- name: Try to remove user 'janet' that doesn't exist
|
||||
community.general.homectl:
|
||||
name: janet
|
||||
state: absent
|
||||
register: user_not_exist
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Use check_mode to try and create user 'diana'
|
||||
community.general.homectl:
|
||||
name: diana
|
||||
password: helloworld123!@
|
||||
state: present
|
||||
check_mode: yes
|
||||
register: diana_create_checkmode_out
|
||||
|
||||
- name: Verify user 'diana' was not created with check_mode
|
||||
command: homectl inspect diana
|
||||
register: user_diana_exists
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Try to modify user 'jake' with only noexec mount option in check_mode
|
||||
community.general.homectl:
|
||||
name: jake
|
||||
password: myreallysecurepassword12!
|
||||
state: present
|
||||
mountopts: noexec
|
||||
check_mode: yes
|
||||
register: jake_checkmode_out
|
||||
|
||||
- name: Verify user 'jake' was not modified and still has all mount options
|
||||
command: homectl inspect jake
|
||||
register: user_jake_details_out
|
||||
|
||||
- name: Modify user 'jake' with only noexec mount option
|
||||
community.general.homectl:
|
||||
name: jake
|
||||
password: myreallysecurepassword12!
|
||||
state: present
|
||||
mountopts: noexec
|
||||
register: jake_modify_out
|
||||
|
||||
- name: modify user 'jake' again with only noexec mount option to make sure changed is false as nothing has changed.
|
||||
community.general.homectl:
|
||||
name: jake
|
||||
password: myreallysecurepassword12!
|
||||
state: present
|
||||
mountopts: noexec
|
||||
register: jake_modify_again_out
|
||||
|
||||
- name: Try to modify user 'jake' with an incorrect password
|
||||
community.general.homectl:
|
||||
name: jake
|
||||
password: incorrectPassword!
|
||||
state: present
|
||||
mountopts: noexec
|
||||
locked: yes
|
||||
ignore_errors: yes
|
||||
register: jake_incorrect_pass_out
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- james_info.rc == 0
|
||||
- tom_userinfo.data['gid'] == 1000 and tom_userinfo.data['uid'] == 1000
|
||||
- user_exists is changed and user_exists.data['shell'] == '/bin/ksh'
|
||||
- resize_out is not changed
|
||||
- disk_out is changed
|
||||
- delete_foobar_out is changed
|
||||
- danielle_out is not changed
|
||||
- lukuser_modify_out.data['timeZone'] == "America/New_York" and lukuser_modify_out.data['shell'] == "/bin/zsh"
|
||||
- user_not_exist is not changed and user_not_exist.msg == "User does not exist!"
|
||||
- jake_out is changed and jake_out.data['mountNoDevices'] == True and jake_out.data['mountNoSuid'] == True and jake_out.data['mountNoExecute'] == True
|
||||
- diana_create_checkmode_out is changed and 'No home for user diana known' in user_diana_exists.stderr
|
||||
- "jake_checkmode_out is changed and 'Mount Flags: nosuid nodev noexec' in user_jake_details_out.stdout"
|
||||
- jake_modify_out is changed and jake_modify_out.data['privileged']['sshAuthorizedKeys'] is not none
|
||||
- jake_modify_out.data['mountNoDevices'] == False and jake_modify_out.data['mountNoExecute'] == True and jake_modify_out.data['mountNoSuid'] == False
|
||||
- jake_modify_again_out is not changed
|
||||
- jake_incorrect_pass_out is not changed and jake_incorrect_pass_out is failed and jake_incorrect_pass_out.msg == 'User exists but password is incorrect!'
|
||||
|
||||
# homectl was first introduced in systemd 245 so check version >= 245 and make sure system has systemd and homectl command
|
||||
when: systemd_version.rc == 0 and (systemd_version.stdout | regex_search('[0-9][0-9][0-9]') | int >= 245) and homectl_version.rc == 0
|
Loading…
Reference in a new issue