diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/action_plugins/fetch_cmd.py | 62 | ||||
-rw-r--r-- | lib/action_plugins/openldap.py | 86 | ||||
-rw-r--r-- | lib/modules/fetch_cmd.py | 56 | ||||
-rw-r--r-- | lib/modules/mysql_user2 | 491 | ||||
-rw-r--r-- | lib/modules/openldap | 82 | ||||
-rw-r--r-- | lib/modules/postmap | 8 | ||||
-rw-r--r-- | lib/modules/postmulti | 8 |
7 files changed, 203 insertions, 590 deletions
diff --git a/lib/action_plugins/fetch_cmd.py b/lib/action_plugins/fetch_cmd.py new file mode 100644 index 0000000..93960eb --- /dev/null +++ b/lib/action_plugins/fetch_cmd.py @@ -0,0 +1,62 @@ +# Fetch the output of a remote command +# Copyright (c) 2016 Guilhem Moulin <guilhem@fripost.org> +# +# This program 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. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +import subprocess, os +from ansible.plugins.action import ActionBase +from ansible.utils.path import makedirs_safe +from ansible.utils.hashing import checksum + +class ActionModule(ActionBase): + TRANSFERS_FILES = True + + def run(self, tmp=None, task_vars=None): + if task_vars is None: + task_vars = dict() + + if self._play_context.check_mode: + return dict(skipped=True, msg='check mode not supported for this module') + + result = super(ActionModule, self).run(tmp, task_vars) + + cmd = self._task.args.get('cmd', None) + stdin = self._task.args.get('stdin', None) + dest = self._task.args.get('dest', None) + + if cmd is None or dest is None: + return dict(failed=True, msg="cmd and dest are required") + + if stdin is not None: + stdin = self._connection._shell.join_path(stdin) + stdin = self._remote_expand_user(stdin) + + remote_user = task_vars.get('ansible_ssh_user') or self._play_context.remote_user + stdout = self._connection._shell.join_path(self._make_tmp_path(remote_user), 'stdout') + result.update(self._execute_module(module_args=dict(cmd=cmd, stdin=stdin, dest=stdout), task_vars=task_vars)) + + # calculate checksum for the local file + local_checksum = checksum(dest) + + # calculate checksum for the remote file, don't bother if using become as slurp will be used + remote_checksum = self._execute_remote_stat(stdout, all_vars=task_vars, follow=True).get('checksum') + + if remote_checksum != local_checksum: + makedirs_safe(os.path.dirname(dest)) + self._connection.fetch_file(stdout, dest) + if checksum(dest) == remote_checksum: + result.update(dict(changed=True)) + else: + result.update(dict(failed=True)) + return result diff --git a/lib/action_plugins/openldap.py b/lib/action_plugins/openldap.py index 5dbf59f..b94a822 100644 --- a/lib/action_plugins/openldap.py +++ b/lib/action_plugins/openldap.py @@ -1,86 +1,72 @@ # Manage OpenLDAP databases # Copyright (c) 2014 Guilhem Moulin <guilhem@fripost.org> # # This program 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. # # This program 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 this program. If not, see <http://www.gnu.org/licenses/>. -import os -import pipes -import tempfile +from ansible.plugins.action import ActionBase +from ansible.module_utils._text import to_text -from ansible.utils import template -from ansible import utils -from ansible.runner.return_data import ReturnData - -class ActionModule(object): +class ActionModule(ActionBase): TRANSFERS_FILES = True - def __init__(self, runner): - self.runner = runner - - def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs): - ''' handler for file transfer operations ''' + def run(self, tmp=None, task_vars=None): + if task_vars is None: + task_vars = dict() - if self.runner.noop_on_check(inject): - return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True)) + if self._play_context.check_mode: + return dict(skipped=True, msg='check mode not supported for this module') - # load up options - options = {} - if complex_args: - options.update(complex_args) - options.update(utils.parse_kv(module_args)) + result = super(ActionModule, self).run(tmp, task_vars) - target = options.get('target', None) - local = options.get('local', 'no') + target = self._task.args.get('target', None) + local = self._task.args.get('local', 'no') if local not in [ 'no', 'file', 'template' ]: - result = dict(failed=True, msg="local must be in ['no','file','template']") - return ReturnData(conn=conn, comm_ok=False, result=result) + return dict(failed=True, msg="local must be in ['no','file','template']") if local != 'no' and target is None: - result = dict(failed=True, msg="target is required in local mode") - return ReturnData(conn=conn, comm_ok=False, result=result) + return dict(failed=True, msg="target is required in local mode") if local == 'no': # run the module remotely - return self.runner._execute_module(conn, tmp, 'openldap', module_args, inject=inject, complex_args=complex_args) - elif '_original_file' in inject: - target = utils.path_dwim_relative(inject['_original_file'], local+'s', target, self.runner.basedir) + return self._execute_module(module_args=self._task.args, task_vars=task_vars) + + if self._task._role is not None: + target = self._loader.path_dwim_relative(self._task._role._role_path, local+'s', target) else: - # the source is local, so expand it here - target = os.path.expanduser(target) + target = self._loader.path_dwim_relative(self._loader.get_basedir(), local+'s', target) + + remote_user = task_vars.get('ansible_ssh_user') or self._play_context.remote_user + new_module_args = self._task.args.copy() + new_module_args['target'] = self._connection._shell.join_path(self._make_tmp_path(remote_user), 'target.ldif') + new_module_args['local'] = 'no' - options['local'] = 'no' - options['target'] = os.path.join(tmp, os.path.basename(target)) if local == 'template': - # template the source data locally and transfer it + # template the source data locally try: - s = template.template_from_file(self.runner.basedir, target, inject, vault_password=self.runner.vault_pass) - tmpfile = tempfile.NamedTemporaryFile(delete=False) - tmpfile.write(s) - tmpfile.close() - target = tmpfile.name - except Exception, e: - result = dict(failed=True, msg=str(e)) - return ReturnData(conn=conn, comm_ok=False, result=result) - conn.put_file(tmpfile.name, options['target']) - os.unlink(tmpfile.name) + with open(target, 'r') as f: + template_data = to_text(f.read()) + target = self._templar.template(template_data, preserve_trailing_newlines=True, escape_backslashes=False, convert_data=False) + except Exception as e: + result['failed'] = True + result['msg'] = type(e).__name__ + ": " + str(e) + return result + # transfer the file and run the module remotely + self._transfer_data(new_module_args['target'], target) elif local == 'file': - conn.put_file(target, options['target']) + self._transfer_file(target, new_module_args['target']) - # run the script remotely with the new (temporary) filename - module_args = "" - for o in options: - module_args = "%s %s=%s" % (module_args, o, pipes.quote(options[o])) - return self.runner._execute_module(conn, tmp, 'openldap', module_args, inject=inject) + result.update(self._execute_module(module_args=new_module_args, task_vars=task_vars)) + return result diff --git a/lib/modules/fetch_cmd.py b/lib/modules/fetch_cmd.py new file mode 100644 index 0000000..ca3e817 --- /dev/null +++ b/lib/modules/fetch_cmd.py @@ -0,0 +1,56 @@ +#!/usr/bin/python3 + +# Fetch the output of a remote command +# Copyright (c) 2016 Guilhem Moulin <guilhem@fripost.org> +# +# This program 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. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + + +# import module snippets +from ansible.module_utils.basic import * + +def main(): + module = AnsibleModule( + argument_spec = dict( + cmd = dict( default=None ), + stdin = dict( default=None ), + dest = dict( default=None ), + ), + supports_check_mode=False + ) + + params = module.params + cmd = params['cmd'] + stdin = params['stdin'] + dest = params['dest'] + + if cmd is None or dest is None: + return dict(failed=True, msg="cmd and dest are required") + + changed = False + try: + if stdin is not None: + stdin = open(stdin, 'r') + + with open(dest, 'w') as stdout: + subprocess.check_call(cmd.split(), stdin=stdin, stdout=stdout) + if stdin is not None: + stdin.close() + + except KeyError as e: + module.fail_json(msg=str(e)) + + module.exit_json(changed=changed) + +main() diff --git a/lib/modules/mysql_user2 b/lib/modules/mysql_user2 deleted file mode 100644 index d10e3e0..0000000 --- a/lib/modules/mysql_user2 +++ /dev/null @@ -1,491 +0,0 @@ -#!/usr/bin/python - -# (c) 2012, Mark Theunissen <mark.theunissen@gmail.com> -# Sponsored by Four Kitchens http://fourkitchens.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: mysql_user2 -short_description: Adds or removes a user from a MySQL database. -description: - - Adds or removes a user from a MySQL database. -version_added: "0.6" -options: - name: - description: - - name of the user (role) to add or remove - required: true - default: null - password: - description: - - set the user's password - required: false - default: null - host: - description: - - the 'host' part of the MySQL username - required: false - default: localhost - login_user: - description: - - The username used to authenticate with - required: false - default: null - login_password: - description: - - The password used to authenticate with - required: false - default: null - login_host: - description: - - Host running the database - required: false - default: localhost - login_port: - description: - - Port of the MySQL server - required: false - default: 3306 - version_added: '1.4' - login_unix_socket: - description: - - The path to a Unix domain socket for local connections - required: false - default: null - priv: - description: - - "MySQL privileges string in the format: C(db.table:priv1,priv2)" - required: false - default: null - append_privs: - description: - - Append the privileges defined by priv to the existing ones for this - user instead of overwriting existing ones. - required: false - choices: [ "yes", "no" ] - default: "no" - version_added: "1.4" - state: - description: - - Whether the user should exist. When C(absent), removes - the user. - required: false - default: present - choices: [ "present", "absent" ] - check_implicit_admin: - description: - - Check if mysql allows login as root/nopassword before trying supplied credentials. - required: false - default: false - version_added: "1.3" -notes: - - Requires the MySQLdb Python package on the remote host. For Ubuntu, this - is as easy as apt-get install python-mysqldb. - - Both C(login_password) and C(login_username) are required when you are - passing credentials. If none are present, the module will attempt to read - the credentials from C(~/.my.cnf), and finally fall back to using the MySQL - default login of 'root' with no password. - - "MySQL server installs with default login_user of 'root' and no password. To secure this user - as part of an idempotent playbook, you must create at least two tasks: the first must change the root user's password, - without providing any login_user/login_password details. The second must drop a ~/.my.cnf file containing - the new root credentials. Subsequent runs of the playbook will then succeed by reading the new credentials from - the file." - -requirements: [ "ConfigParser", "MySQLdb" ] -author: Mark Theunissen -''' - -EXAMPLES = """ -# Create database user with name 'bob' and password '12345' with all database privileges -- mysql_user: name=bob password=12345 priv=*.*:ALL state=present - -# Ensure no user named 'sally' exists, also passing in the auth credentials. -- mysql_user: login_user=root login_password=123456 name=sally state=absent - -# Example privileges string format -mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanotherdb.*:ALL - -# Example using login_unix_socket to connect to server -- mysql_user: name=root password=abc123 login_unix_socket=/var/run/mysqld/mysqld.sock - -# Example .my.cnf file for setting the root password -# Note: don't use quotes around the password, because the mysql_user module -# will include them in the password but the mysql client will not - -[client] -user=root -password=n<_665{vS43y -""" - -import ConfigParser -import getpass -import tempfile -try: - import MySQLdb -except ImportError: - mysqldb_found = False -else: - mysqldb_found = True - -# =========================================== -# MySQL module specific support methods. -# - -def user_exists(cursor, user, host): - cursor.execute("SELECT count(*) FROM user WHERE user = %s AND host = %s", (user,host)) - count = cursor.fetchone() - return count[0] > 0 - -def load_plugin(cursor, plugin): - cursor.execute("SELECT count(*) FROM information_schema.plugins WHERE name = %s", plugin) - count = cursor.fetchone() - if count[0] == 0: - so = "%s.so" % plugin - cursor.execute("INSTALL PLUGIN %s SONAME %s", (plugin, so)) - -def user_add(cursor, user, host, password, new_priv, auth_plugin): - if password is None: - # Automatically loaded on first first use. - load_plugin(cursor, auth_plugin) - cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s", (user,host,auth_plugin)) - else: - cursor.execute("CREATE USER %s@%s IDENTIFIED BY %s", (user,host,password)) - if new_priv is not None: - for db_table, priv in new_priv.iteritems(): - privileges_grant(cursor, user,host,db_table,priv) - return True - -def user_mod(cursor, user, host, password, new_priv, append_privs, auth_plugin): - changed = False - grant_option = False - - # Handle plugin. - if auth_plugin is not None: - cursor.execute("SELECT plugin FROM user WHERE user = %s AND host = %s", (user,host)) - if cursor.fetchone()[0] != auth_plugin: - # Sadly there is no proper way to updade the authentication plugin: - # http://bugs.mysql.com/bug.php?id=67449 - cursor.execute( "UPDATE user SET plugin = %s, password = '' WHERE user = %s AND host = %s" - , (auth_plugin,user,host)) - cursor.execute("FLUSH PRIVILEGES") - changed = True - - # Handle passwords. - if password is not None: - cursor.execute("SELECT password FROM user WHERE user = %s AND host = %s", (user,host)) - current_pass_hash = cursor.fetchone() - cursor.execute("SELECT PASSWORD(%s)", (password,)) - new_pass_hash = cursor.fetchone() - if current_pass_hash[0] != new_pass_hash[0]: - cursor.execute("SET PASSWORD FOR %s@%s = PASSWORD(%s)", (user,host,password)) - changed = True - - # Handle privileges. - if new_priv is not None: - curr_priv = privileges_get(cursor, user,host) - - # If the user has privileges on a db.table that doesn't appear at all in - # the new specification, then revoke all privileges on it. - for db_table, priv in curr_priv.iteritems(): - # If the user has the GRANT OPTION on a db.table, revoke it first. - if "GRANT" in priv: - grant_option = True - if db_table not in new_priv: - if user != "root" and "PROXY" not in priv and not append_privs: - privileges_revoke(cursor, user,host,db_table,grant_option) - changed = True - - # If the user doesn't currently have any privileges on a db.table, then - # we can perform a straight grant operation. - for db_table, priv in new_priv.iteritems(): - if db_table not in curr_priv: - privileges_grant(cursor, user,host,db_table,priv) - changed = True - - # If the db.table specification exists in both the user's current privileges - # and in the new privileges, then we need to see if there's a difference. - db_table_intersect = set(new_priv.keys()) & set(curr_priv.keys()) - for db_table in db_table_intersect: - priv_diff = set(new_priv[db_table]) ^ set(curr_priv[db_table]) - if (len(priv_diff) > 0): - privileges_revoke(cursor, user,host,db_table,grant_option) - privileges_grant(cursor, user,host,db_table,new_priv[db_table]) - changed = True - - return changed - -def user_delete(cursor, user, host): - cursor.execute("DROP USER %s@%s", (user,host)) - return True - -def privileges_get(cursor, user,host): - """ MySQL doesn't have a better method of getting privileges aside from the - SHOW GRANTS query syntax, which requires us to then parse the returned string. - Here's an example of the string that is returned from MySQL: - - GRANT USAGE ON *.* TO 'user'@'localhost' IDENTIFIED BY 'pass'; - - This function makes the query and returns a dictionary containing the results. - The dictionary format is the same as that returned by privileges_unpack() below. - """ - output = {} - cursor.execute("SHOW GRANTS FOR %s@%s", (user,host)) - grants = cursor.fetchall() - - def pick(x): - if x == 'ALL PRIVILEGES': - return 'ALL' - else: - return x - - for grant in grants: - res = re.match("GRANT (.+) ON (.+) TO '.+'@'.+'( IDENTIFIED BY PASSWORD '.+')? ?(.*)", grant[0]) - if res is None: - module.fail_json(msg="unable to parse the MySQL grant string") - privileges = res.group(1).split(", ") - privileges = [ pick(x) for x in privileges] - if "WITH GRANT OPTION" in res.group(4): - privileges.append('GRANT') - db = res.group(2) - output[db] = privileges - return output - -def privileges_unpack(priv): - """ Take a privileges string, typically passed as a parameter, and unserialize - it into a dictionary, the same format as privileges_get() above. We have this - custom format to avoid using YAML/JSON strings inside YAML playbooks. Example - of a privileges string: - - mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanother.*:ALL - - The privilege USAGE stands for no privileges, so we add that in on *.* if it's - not specified in the string, as MySQL will always provide this by default. - """ - output = {} - for item in priv.split('/'): - pieces = item.split(':') - if pieces[0].find('.') != -1: - pieces[0] = pieces[0].split('.') - for idx, piece in enumerate(pieces): - if pieces[0][idx] != "*": - pieces[0][idx] = "`" + pieces[0][idx] + "`" - pieces[0] = '.'.join(pieces[0]) - - output[pieces[0]] = [ g.strip() for g in pieces[1].upper().split(',') ] - - if '*.*' not in output: - output['*.*'] = ['USAGE'] - - return output - -def privileges_revoke(cursor, user,host,db_table,grant_option): - if grant_option: - query = "REVOKE GRANT OPTION ON %s FROM '%s'@'%s'" % (db_table,user,host) - cursor.execute(query) - query = "REVOKE ALL PRIVILEGES ON %s FROM '%s'@'%s'" % (db_table,user,host) - cursor.execute(query) - -def privileges_grant(cursor, user,host,db_table,priv): - - priv_string = ",".join(filter(lambda x: x != 'GRANT', priv)) - query = "GRANT %s ON %s TO '%s'@'%s'" % (priv_string,db_table,user,host) - if 'GRANT' in priv: - query = query + " WITH GRANT OPTION" - cursor.execute(query) - - -def strip_quotes(s): - """ Remove surrounding single or double quotes - - >>> print strip_quotes('hello') - hello - >>> print strip_quotes('"hello"') - hello - >>> print strip_quotes("'hello'") - hello - >>> print strip_quotes("'hello") - 'hello - - """ - single_quote = "'" - double_quote = '"' - - if s.startswith(single_quote) and s.endswith(single_quote): - s = s.strip(single_quote) - elif s.startswith(double_quote) and s.endswith(double_quote): - s = s.strip(double_quote) - return s - - -def config_get(config, section, option): - """ Calls ConfigParser.get and strips quotes - - See: http://dev.mysql.com/doc/refman/5.0/en/option-files.html - """ - return strip_quotes(config.get(section, option)) - - -def _safe_cnf_load(config, path): - - data = {'user':'', 'password':''} - - # read in user/pass - f = open(path, 'r') - for line in f.readlines(): - line = line.strip() - if line.startswith('user='): - data['user'] = line.split('=', 1)[1].strip() - if line.startswith('password=') or line.startswith('pass='): - data['password'] = line.split('=', 1)[1].strip() - f.close() - - # write out a new cnf file with only user/pass - fh, newpath = tempfile.mkstemp(prefix=path + '.') - f = open(newpath, 'wb') - f.write('[client]\n') - f.write('user=%s\n' % data['user']) - f.write('password=%s\n' % data['password']) - f.close() - - config.readfp(open(newpath)) - os.remove(newpath) - return config - -def load_mycnf(): - config = ConfigParser.RawConfigParser() - mycnf = os.path.expanduser('~/.my.cnf') - if not os.path.exists(mycnf): - return False - try: - config.readfp(open(mycnf)) - except (IOError): - return False - except: - config = _safe_cnf_load(config, mycnf) - - # We support two forms of passwords in .my.cnf, both pass= and password=, - # as these are both supported by MySQL. - try: - passwd = config_get(config, 'client', 'password') - except (ConfigParser.NoOptionError): - try: - passwd = config_get(config, 'client', 'pass') - except (ConfigParser.NoOptionError): - return False - - # If .my.cnf doesn't specify a user, default to user login name - try: - user = config_get(config, 'client', 'user') - except (ConfigParser.NoOptionError): - user = getpass.getuser() - creds = dict(user=user,passwd=passwd) - return creds - -def connect(module, login_user, login_password): - if module.params["login_unix_socket"]: - db_connection = MySQLdb.connect(host=module.params["login_host"], unix_socket=module.params["login_unix_socket"], user=login_user, passwd=login_password, db="mysql") - else: - db_connection = MySQLdb.connect(host=module.params["login_host"], port=int(module.params["login_port"]), user=login_user, passwd=login_password, db="mysql") - return db_connection.cursor() - -# =========================================== -# Module execution. -# - -def main(): - module = AnsibleModule( - argument_spec = dict( - login_user=dict(default=None), - login_password=dict(default=None), - login_host=dict(default="localhost"), - login_port=dict(default="3306"), - login_unix_socket=dict(default=None), - user=dict(required=True, aliases=['name']), - password=dict(default=None), - host=dict(default="localhost"), - state=dict(default="present", choices=["absent", "present"]), - priv=dict(default=None), - append_privs=dict(type="bool", default="no"), - check_implicit_admin=dict(default=False), - auth_plugin=dict(default=None) - ) - ) - user = module.params["user"] - password = module.params["password"] - host = module.params["host"] - state = module.params["state"] - priv = module.params["priv"] - check_implicit_admin = module.params['check_implicit_admin'] - append_privs = module.boolean(module.params["append_privs"]) - auth_plugin = module.params['auth_plugin'] - - if not mysqldb_found: - module.fail_json(msg="the python mysqldb module is required") - - if priv is not None: - try: - priv = privileges_unpack(priv) - except: - module.fail_json(msg="invalid privileges string") - - # Either the caller passes both a username and password with which to connect to - # mysql, or they pass neither and allow this module to read the credentials from - # ~/.my.cnf. - login_password = module.params["login_password"] - login_user = module.params["login_user"] - if login_user is None and login_password is None: - mycnf_creds = load_mycnf() - if mycnf_creds is False: - login_user = "root" - login_password = "" - else: - login_user = mycnf_creds["user"] - login_password = mycnf_creds["passwd"] - elif login_password is None or login_user is None: - module.fail_json(msg="when supplying login arguments, both login_user and login_password must be provided") - - cursor = None - try: - if check_implicit_admin: - try: - cursor = connect(module, 'root', '') - except: - pass - - if not cursor: - cursor = connect(module, login_user, login_password) - except Exception, e: - module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or ~/.my.cnf has the credentials") - - if state == "present": - if user_exists(cursor, user, host): - changed = user_mod(cursor, user, host, password, priv, append_privs, auth_plugin) - else: - if (password is None and auth_plugin is None) or (password is not None and auth_plugin is not None): - module.fail_json(msg="password xor auth_plugin is required when adding a user") - changed = user_add(cursor, user, host, password, priv, auth_plugin) - elif state == "absent": - if user_exists(cursor, user, host): - changed = user_delete(cursor, user, host) - else: - changed = False - module.exit_json(changed=changed, user=user) - -# this is magic, see lib/ansible/module_common.py -#<<INCLUDE_ANSIBLE_MODULE_COMMON>> -main() diff --git a/lib/modules/openldap b/lib/modules/openldap index 5178033..c09e791 100644 --- a/lib/modules/openldap +++ b/lib/modules/openldap @@ -1,21 +1,21 @@ -#!/usr/bin/python +#!/usr/bin/python3 # Manage OpenLDAP databases # Copyright (c) 2013 Guilhem Moulin <guilhem@fripost.org> # # This program 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. # # This program 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 this program. If not, see <http://www.gnu.org/licenses/>. import ldap, ldap.sasl from ldap.filter import filter_format from ldap.dn import dn2str,explode_dn,str2dn @@ -32,162 +32,162 @@ import tempfile, atexit # explicit indices to what we find in the LDIF. indexedAttributes = frozenset([ 'olcAttributeTypes', 'olcObjectClasses', 'olcAccess', 'olcSyncrepl', 'olcOverlay', 'olcLimits', 'olcAuthzRegexp', 'olcDbConfig', ]) # Another hack. Configuration entries sometimes pollutes the DNs with # indices, thus it's not possible to directly use them as base. # Instead, we use their parent as a base, and search for the *unique* # match with the same ObjectClass and the matching extra attributes. # ('%s' in the attribute value is replaced with the value of the source # entry.) indexedDN = { - 'olcSchemaConfig': [('cn', '{*}%s')], - 'olcMdbConfig': [('olcDbDirectory', '%s' )], - 'olcOverlayConfig': [('olcOverlay', '%s' )], - 'olcMonitorConfig': [], + b'olcSchemaConfig': [('cn', '{*}%s')], + b'olcMdbConfig': [('olcDbDirectory', '%s' )], + b'olcOverlayConfig': [('olcOverlay', '%s' )], + b'olcMonitorConfig': [], } # Allow for flexible ACLs for user using SASL's EXTERNAL mechanism. # "username=postfix,cn=peercred,cn=external,cn=auth" is replaced by # "gidNumber=106+uidNumber=102,cn=peercred,cn=external,cn=auth" where # 102 is postfix's UID and 106 its primary GID. # (Regular expressions are not allowed.) -sasl_ext_re = re.compile( r"""(?P<start>\sby\s+dn(?:\.exact)?)= - (?P<quote>['\"]?)username=(?P<user>[a-z][-a-z0-9_]*), +sasl_ext_re = re.compile( b"""(?P<start>\sby\s+dn(?:\.exact)?)= + (?P<quote>['\"]?)username=(?P<user>_?[a-z][-a-z0-9_]*), (?P<end>cn=peercred,cn=external,cn=auth) (?P=quote)\s""" , re.VERBOSE ) -multispaces = re.compile( r"\s+" ) +multispaces = re.compile( b"\s+" ) pwd_dict = {} def acl_sasl_ext(m): - u = m.group('user') + u = m.group('user').decode("utf-8") if u not in pwd_dict.keys(): pwd_dict[u] = pwd.getpwnam(u) - return '%s="gidNumber=%d+uidNumber=%d,%s" ' % ( m.group('start') - , pwd_dict[u].pw_gid - , pwd_dict[u].pw_uid - , m.group('end') - ) + return b'%s="gidNumber=%d+uidNumber=%d,%s" ' % ( m.group('start') + , pwd_dict[u].pw_gid + , pwd_dict[u].pw_uid + , m.group('end') + ) # Run the given callback on each DN seen. If its return value is not # None, update the changed variable. class LDIFCallback(LDIFParser): def __init__(self, module, input, callback): LDIFParser.__init__(self,input) self.callback = callback self.changed = False def handle(self,dn,entry): b = self.callback(dn,entry) if b is not None: self.changed |= b # Check if the given dn is already present in the directory. # Returns None if doesn't exist, and give the dn,entry otherwise def flexibleSearch(module, l, dn, entry): idxClasses = set(entry['objectClass']).intersection(indexedDN.keys()) if not idxClasses: base = dn scope = ldap.SCOPE_BASE f = 'objectClass=*' else: # Search on the parent instead, and try to use a precise filter dn = str2dn(dn) h,t,_ = dn.pop(0)[0] base = dn2str(dn) scope = ldap.SCOPE_ONELEVEL f = [] for c in idxClasses: - f.append ( filter_format('objectClass=%s', [c]) ) + f.append ( filter_format('objectClass=%s', [c.decode("utf-8")]) ) for a,v in indexedDN[c]: if a == h: v2 = t elif a not in entry.keys() or len(entry[a]) > 1: module.fail_json(msg="Multiple values found! This is a bug. Please report.") else: - v2 = entry[a][0] + v2 = entry[a][0].decode("utf-8") f.append ( filter_format(a+'='+v, [v2]) ) if len(f) == 1: f = f[0] else: f = '(&(' + ')('.join(f) + '))' r = l.search_s( base, scope, filterstr=f ) if len(r) > 1: module.fail_json(msg="Multiple results found! This is a bug. Please report.") elif r: return r.pop() # Add or modify (only the attributes that differ from those in the # directory) the entry for that DN. # l must be an LDAPObject, and should provide an open connection to the # directory with disclose/search/write access. def processEntry(module, l, dn, entry): changed = False for x in indexedAttributes.intersection(entry.keys()): # remove useless extra spaces in ACLs etc - entry[x] = map( partial(multispaces.sub, ' '), entry[x] ) + entry[x] = list(map( partial(multispaces.sub, b' '), entry[x] )) r = flexibleSearch( module, l, dn, entry ) if r is None: changed = True if module.check_mode: module.exit_json(changed=changed, msg="add DN %s" % dn) if 'olcAccess' in entry.keys(): # replace "username=...,cn=peercred,cn=external,cn=auth" # by a DN with proper gidNumber and uidNumber - entry['olcAccess'] = map ( partial(sasl_ext_re.sub, acl_sasl_ext) - , entry['olcAccess'] ) + entry['olcAccess'] = list(map ( partial(sasl_ext_re.sub, acl_sasl_ext) + , entry['olcAccess'] )) l.add_s( dn, addModlist(entry) ) else: d,e = r fst = str2dn(dn).pop(0)[0][0] diff = [] - for a,v in e.iteritems(): + for a,v in e.items(): if a not in entry.keys(): if a != fst: # delete all values except for the first attribute, # which is implicit diff.append(( ldap.MOD_DELETE, a, None )) elif a in indexedAttributes: if a == 'olcAccess': # replace "username=...,cn=peercred,cn=external,cn=auth" # by a DN with proper gidNumber and uidNumber - entry[a] = map ( partial(sasl_ext_re.sub, acl_sasl_ext) - , entry[a] ) + entry[a] = list(map ( partial(sasl_ext_re.sub, acl_sasl_ext) + , entry[a] )) # add explicit indices in the entry from the LDIF - entry[a] = map( (lambda x: '{%d}%s' % x) - , zip(range(len(entry[a])),entry[a]) ) + entry[a] = list(map( (lambda x: b'{%d}%s' % x) + , zip(range(len(entry[a])),entry[a]))) if v != entry[a]: diff.append(( ldap.MOD_REPLACE, a, entry[a] )) elif v != entry[a]: # for non-indexed attribute, we update values in the # symmetric difference only s1 = set(v) s2 = set(entry[a]) if s1.isdisjoint(s2): # replace the former values with the new ones diff.append(( ldap.MOD_REPLACE, a, entry[a] )) else: x = list(s1.difference(s2)) if x: diff.append(( ldap.MOD_DELETE, a, x )) y = list(s2.difference(s1)) if y: diff.append(( ldap.MOD_ADD, a, y )) # add attributes that weren't in e for a in set(entry).difference(e.keys()): @@ -214,65 +214,65 @@ def loadModule(module, l, name): module.exit_json(changed=changed, msg="add module %s" % name) l.modify_s( 'cn=module{0},cn=config' , [(ldap.MOD_ADD, 'olcModuleLoad', name)] ) return changed # Find the database associated with a given attribute (eg, # olcDbDirectory or olcSuffix). def getDN_DB(module, l, a, v, attrlist=['']): f = filter_format( '(&(objectClass=olcDatabaseConfig)('+a+'=%s))', [v] ) return l.search_s( 'cn=config' , ldap.SCOPE_ONELEVEL , filterstr = f , attrlist = attrlist ) # Convert a *.schema file into *.ldif format. The algorithm can be found # in /etc/ldap/schema/openldap.ldif . def slapd_to_ldif(src, name): - s = open( src, 'r' ) + s = open( src, 'rb' ) d = tempfile.NamedTemporaryFile(delete=False) atexit.register(lambda: os.unlink( d.name )) - d.write('dn: cn=%s,cn=schema,cn=config\n' % name) - d.write('objectClass: olcSchemaConfig\n') + d.write(b'dn: cn=%s,cn=schema,cn=config\n' % name.encode("utf-8")) + d.write(b'objectClass: olcSchemaConfig\n') - re1 = re.compile( r'^objectIdentifier\s(.*)', re.I ) - re2 = re.compile( r'^objectClass\s(.*)', re.I ) - re3 = re.compile( r'^attributeType\s(.*)', re.I ) - reSp = re.compile( r'^\s+' ) + re1 = re.compile( b'^objectIdentifier\s(.*)', re.I ) + re2 = re.compile( b'^objectClass\s(.*)', re.I ) + re3 = re.compile( b'^attributeType\s(.*)', re.I ) + reSp = re.compile( b'^\s+' ) for line in s.readlines(): - if line == '\n': - line = '#\n' + if line == b'\n': + line = b'#\n' m1 = re1.match(line) m2 = re2.match(line) m3 = re3.match(line) if m1 is not None: - line = 'olcObjectIdentifier: %s' % m1.group(1) + line = b'olcObjectIdentifier: %s' % m1.group(1) elif m2 is not None: - line = 'olcObjectClasses: %s' % m2.group(1) + line = b'olcObjectClasses: %s' % m2.group(1) elif m3 is not None: - line = 'olcAttributeTypes: %s' % m3.group(1) + line = b'olcAttributeTypes: %s' % m3.group(1) - d.write( reSp.sub(line, ' ') ) + d.write( reSp.sub(line, b' ') ) s.close() d.close() return d.name def main(): module = AnsibleModule( argument_spec = dict( target = dict( default=None ), module = dict( default=None ), suffix = dict( default=None ), format = dict( default="ldif", choices=["ldif","slapd.conf"] ), name = dict( default=None ), local = dict( default="no", choices=["no","file","template"] ), delete = dict( default=None ), ), supports_check_mode=True ) @@ -286,41 +286,41 @@ def main(): delete = params['delete'] changed = False try: if delete is not None: if name is None: module.fail_json(msg="missing name") l = ldap.initialize( 'ldapi://' ) l.sasl_interactive_bind_s('', ldap.sasl.external()) if delete == 'entry': filterStr = '(objectClass=*)' else: filterStr = [ '(%s=*)' % x for x in delete.split(',') ] if len(filterStr) > 1: filterStr = '(|' + ''.join(filterStr) + ')' else: filterStr = filterStr[0] try: r = l.search_s( name, ldap.SCOPE_BASE, filterStr, attrsonly=1 ) - except ldap.LDAPError, ldap.NO_SUCH_OBJECT: + except (ldap.LDAPError, ldap.NO_SUCH_OBJECT): r = None if r: changed = True if module.check_mode: module.exit_json(changed=changed) if delete == 'entry': l.delete_s(r[0][0]) else: attrlist = list(set(r[0][1].keys()) & set(delete.split(','))) l.modify_s(r[0][0], [ (ldap.MOD_DELETE, x, None) for x in attrlist ]) l.unbind_s() else: if form == 'slapd.conf': if name is None: module.fail_json(msg="missing name") target = slapd_to_ldif(target, name) if target is None and mod is None: @@ -335,40 +335,40 @@ def main(): changed |= loadModule (module, l, '%s.la' % mod) if target is None and suffix is None: l.unbind_s() module.exit_json(changed=changed) if target is None or suffix is None: module.fail_json(msg="missing target or suffix") r = getDN_DB(module, l, 'olcSuffix', suffix) if not r: module.fail_json(msg="No database found for suffix %s" % suffix) elif len(r) > 1: module.fail_json(msg="Multiple results found! This is a bug. Please report.") else: d = 'olcOverlay=%s,%s' % (mod, r.pop()[0]) callback = lambda _,e: processEntry(module,l,d,e) parser = LDIFCallback( module, open(target, 'r'), callback ) parser.parse() changed = parser.changed l.unbind_s() - except subprocess.CalledProcessError, e: + except subprocess.CalledProcessError as e: module.fail_json(rv=e.returncode, msg=e.output.rstrip()) - except ldap.LDAPError, e: + except ldap.LDAPError as e: e = e.args[0] if 'info' in e.keys(): msg = e['info'] elif 'desc' in e.keys(): msg = e['desc'] else: msg = str(e) module.fail_json(msg=msg) - except KeyError, e: + except KeyError as e: module.fail_json(msg=str(e)) module.exit_json(changed=changed) # import module snippets from ansible.module_utils.basic import * main() diff --git a/lib/modules/postmap b/lib/modules/postmap index 7080b25..ce09018 100644 --- a/lib/modules/postmap +++ b/lib/modules/postmap @@ -1,21 +1,21 @@ -#!/usr/bin/python +#!/usr/bin/python3 # Create or update postfix's alias and lookup tables # Copyright (c) 2013 Guilhem Moulin <guilhem@fripost.org> # # This program 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. # # This program 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 this program. If not, see <http://www.gnu.org/licenses/>. try: import selinux HAVE_SELINUX=True @@ -25,85 +25,85 @@ except ImportError: # Look up for the file suffix corresponding to 'db'. If 'db' is unset, # pick the default_detabase_type of the given instance instead. def file_suffix(instance, db): if not db: if instance: cmd = [ os.path.join(os.sep, 'usr', 'sbin', 'postmulti') , '-x' , '-i', instance , '--' ] else: cmd = [] cmd.extend([ os.path.join(os.sep, 'usr', 'sbin', 'postconf') , '-h', 'default_database_type' ]) null = open (os.devnull, 'wb') db = subprocess.check_output(cmd, stderr=null).rstrip() null.closed # See postmap(1) and postalias(1) - suffixes = { 'btree': 'db', 'cdb': 'cdb', 'hash': 'db' } + suffixes = { 'btree': 'db', 'cdb': 'cdb', 'hash': 'db', 'lmdb': 'lmdb' } return suffixes[db] # Compile the given (alias/lookup) table def compile(cmd, instance, db, src): cmd = [ os.path.join(os.sep, 'usr', 'sbin', cmd) ] if instance: config = os.path.join(os.sep, 'etc', 'postfix-%s' % instance) cmd.extend([ '-c', config ]) if db: src = "%s:%s" % (db,src) cmd.append(src) subprocess.check_output(cmd, stderr=subprocess.STDOUT) def main(): module = AnsibleModule( argument_spec = dict( src = dict( required=True ), - db = dict( choices=['btree','cdb','hash'] ), + db = dict( choices=['btree','cdb','hash','lmdb'] ), cmd = dict( choices=['postmap','postalias'], default='postmap' ), instance = dict( required=False ) ), add_file_common_args=True, supports_check_mode=True ) params = module.params src = params['src'] db = params['db'] cmd = params['cmd'] instance = params['instance'] if os.path.isabs(src): src = src else: module.fail_json(msg="absolute paths are required") if not os.path.exists(src): module.fail_json(src=src, msg="no such file") try: dst = "%s.%s" % (src, file_suffix(instance, db)) params['dest'] = dst file_args = module.load_file_common_arguments(params) changed = False msg = None if not os.path.exists(dst) or os.path.getmtime(dst) <= os.path.getmtime(src): changed = True if not module.check_mode: msg = compile( cmd, instance, db, src) - except subprocess.CalledProcessError, e: + except subprocess.CalledProcessError as e: module.fail_json(rv=e.returncode, msg=e.output.rstrip()) changed = module.set_file_attributes_if_different(file_args, changed) module.exit_json(changed=changed, msg=msg) # this is magic, see lib/ansible/module_common.py #<<INCLUDE_ANSIBLE_MODULE_COMMON>> main() diff --git a/lib/modules/postmulti b/lib/modules/postmulti index d6ecb09..e6f58e3 100644 --- a/lib/modules/postmulti +++ b/lib/modules/postmulti @@ -1,103 +1,103 @@ -#!/usr/bin/python +#!/usr/bin/python3 # Create and manage postfix instances. # Copyright (c) 2013 Guilhem Moulin <guilhem@fripost.org> # # This program 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. # # This program 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 this program. If not, see <http://www.gnu.org/licenses/>. # Look up postfix configuration variable def postconf(k, instance=None): if instance: cmd = [ os.path.join(os.sep, 'usr', 'sbin', 'postmulti') , '-x' , '-i', instance , '--' ] else: cmd = [] cmd.extend([ os.path.join(os.sep, 'usr', 'sbin', 'postconf') , '-h', k ]) - return subprocess.check_output(cmd, stderr=subprocess.STDOUT).rstrip() + return subprocess.check_output(cmd, stderr=subprocess.STDOUT).rstrip().decode("utf-8") # To destroy an existing instance: # postmulti -e disable -i mx # postmulti -e destroy -i mx def main(): module = AnsibleModule( argument_spec = dict( instance = dict( required=True ), group = dict( required=False ) ), supports_check_mode=True ) params = module.params instance = params['instance'] group = params['group'] changed=False try: enable = postconf('multi_instance_enable') wrapper = postconf('multi_instance_wrapper') - if enable != "yes" or not wrapper: + if enable != "yes" or wrapper == "": # Initiate postmulti changed = True if module.check_mode: module.exit_json(changed=changed, msg="init postmulti") cmd = [ os.path.join(os.sep, 'usr', 'sbin', 'postmulti') ] cmd.extend([ '-e', 'init' ]) subprocess.check_output(cmd, stderr=subprocess.STDOUT).rstrip() instances = postconf('multi_instance_directories').split() if os.path.join(os.sep, 'etc', 'postfix-%s' % instance) not in instances: changed = True # Create the instance if module.check_mode: module.exit_json(changed=changed, msg="create postmulti") cmd = [ os.path.join(os.sep, 'usr', 'sbin', 'postmulti') ] cmd.extend([ '-e', 'create' ]) if group: cmd.extend([ '-G', group ]) cmd.extend([ '-I', 'postfix-%s' % instance ]) subprocess.check_output(cmd, stderr=subprocess.STDOUT).rstrip() elif group != postconf('multi_instance_group', instance): changed = True # Assign a new group, or remove the existing group if module.check_mode: module.exit_json(changed=changed, msg="assign group") cmd = [ os.path.join(os.sep, 'usr', 'sbin', 'postmulti') ] cmd.extend([ '-e', 'assign', '-i', 'postfix-%s' % instance ]) if group: cmd.extend([ '-G', group ]) else: cmd.extend([ '-G', '-' ]) subprocess.check_output(cmd, stderr=subprocess.STDOUT).rstrip() module.exit_json(changed=changed) - except subprocess.CalledProcessError, e: + except subprocess.CalledProcessError as e: module.fail_json(rv=e.returncode, msg=e.output.rstrip()) # this is magic, see lib/ansible/module_common.py #<<INCLUDE_ANSIBLE_MODULE_COMMON>> main() |