From f877db8c189fc0a0c43aa5df9303ad34cceb774e Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 6 Jul 2014 19:10:59 +0200 Subject: Move ansible modules to another directory. --- lib/modules/mysql_user | 491 +++++++++++++++++++++++++++++++++++++++++++++++++ lib/modules/openldap | 434 +++++++++++++++++++++++++++++++++++++++++++ lib/modules/postmap | 109 +++++++++++ lib/modules/postmulti | 103 +++++++++++ lib/mysql_user | 491 ------------------------------------------------- lib/openldap | 434 ------------------------------------------- lib/postmap | 109 ----------- lib/postmulti | 103 ----------- 8 files changed, 1137 insertions(+), 1137 deletions(-) create mode 100644 lib/modules/mysql_user create mode 100644 lib/modules/openldap create mode 100644 lib/modules/postmap create mode 100644 lib/modules/postmulti delete mode 100644 lib/mysql_user delete mode 100644 lib/openldap delete mode 100644 lib/postmap delete mode 100644 lib/postmulti (limited to 'lib') diff --git a/lib/modules/mysql_user b/lib/modules/mysql_user new file mode 100644 index 0000000..64e1f3d --- /dev/null +++ b/lib/modules/mysql_user @@ -0,0 +1,491 @@ +#!/usr/bin/python + +# (c) 2012, Mark Theunissen +# 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 . + +DOCUMENTATION = ''' +--- +module: mysql_user +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 plugin 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 +#<> +main() diff --git a/lib/modules/openldap b/lib/modules/openldap new file mode 100644 index 0000000..7293b23 --- /dev/null +++ b/lib/modules/openldap @@ -0,0 +1,434 @@ +#!/usr/bin/python + +# Manage OpenLDAP databases +# Copyright (c) 2013 Guilhem Moulin +# +# 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 . + +import ldap, ldap.sasl +from ldap.filter import filter_format +from ldap.dn import dn2str,explode_dn,str2dn +from ldap.modlist import addModlist +from ldif import LDIFParser +from functools import partial +import re, pwd +import tempfile, atexit + + +# Dirty hack to check equality between the targetted LDIF and that +# currently in the directory. The value of some configuration (olc*) +# attributes is automatically indexed when added; for those we'll add +# explicit indices to what we find in the LDIF. +indexedAttributes = frozenset([ + 'olcAttributeTypes', + 'olcObjectClasses', + 'olcAccess', + 'olcSyncrepl', + 'olcOverlay', + 'olcLimits', +]) + + +# 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 pase, 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')], + 'olcHdbConfig': [('olcDbDirectory', '%s' )], + 'olcOverlayConfig': [('olcOverlay', '%s' )], +} + +# 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\sby\s+dn(?:\.exact)?)= + (?P['\"]?)username=(?P[a-z][-a-z0-9_]*), + (?Pcn=peercred,cn=external,cn=auth) + (?P=quote)\s""" + , re.VERBOSE ) +multispaces = re.compile( r"\s+" ) +pwd_dict = {} + +def acl_sasl_ext(m): + u = m.group('user') + 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') + ) + + +# 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 + + +# Run slapcat(8) on the given suffix or DB number (suffix takes +# precedence) with an optional filter. (This is useful for offline +# searches, or one needs to bypass ACLs.) Returns an open pipe to the +# subprocess. +def slapcat(filter=None, suffix=None, idx=0): + cmd = [ os.path.join(os.sep, 'usr', 'sbin', 'slapcat') ] + + if filter is not None: + cmd.extend([ '-a', filter ]) + + if suffix is not None: + if type(suffix) is not str: + suffix = dn2str(suffix) + cmd.extend([ '-b', suffix ]) + else: + cmd.append( '-n%d' % idx ) + + return subprocess.Popen( cmd, stdout=subprocess.PIPE + , stderr=open(os.devnull, 'wb') ) + + +# Start / stop / whatever a service. +def service(name, state): + cmd = [ os.path.join(os.sep, 'usr', 'sbin', 'service'), name, state ] + subprocess.check_call( cmd, stdout=open(os.devnull, 'wb') + , stderr=subprocess.STDOUT ) + + +# 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]) ) + 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] + 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] ) + + 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'] ) + l.add_s( dn, addModlist(entry) ) + else: + d,e = r + fst = str2dn(dn).pop(0)[0][0] + diff = [] + for a,v in e.iteritems(): + 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] ) + # add explicit indices in the entry from the LDIF + entry[a] = map( (lambda x: '{%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()): + diff.append(( ldap.MOD_ADD, a, entry[a] )) + + if diff: + changed = True + if module.check_mode: + module.exit_json(changed=changed, msg="mod DN %s" % dn) + l.modify_s( d, diff ) + return changed + + +# Load the given module. +def loadModule(module, l, name): + changed = False + + f = filter_format( '(&(objectClass=olcModuleList)(olcModuleLoad=%s))', [name] ) + r = l.search_s( 'cn=config', ldap.SCOPE_ONELEVEL, filterstr = f, attrlist = [''] ) + + if not r: + changed = True + if module.check_mode: + 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 ) + + +# Clear the given DB directory and delete the associated database. Fail +# if non empty, unless all existing DNS are in skipdns. +def wontRemove(module, skipdns, d, _): + if d not in skipdns: + module.fail_json(msg="won't remove '%s'" % d) +def removeDB(module, dbdir, skipdn=None): + changed = False + if not os.path.exists(dbdir): + return False + + l = ldap.initialize( 'ldapi://' ) + l.sasl_interactive_bind_s('', ldap.sasl.external()) + r = getDN_DB( module, l, 'olcDbDirectory', dbdir, attrlist=['olcSuffix'] ) + l.unbind_s() + + if len(r) > 1: + module.fail_json(msg="Multiple results found! This is a bug. Please report.") + elif r: + dn,entry = r.pop() + suffix = entry['olcSuffix'][0] + + skipdns = [suffix] + if skipdn is not None: + skipdns.extend([ "%s,%s" % (s,suffix) for s in skipdn ]) + # here we need to use slapcat not search_s, because we may + # not have read access on the database (even though we're + # root!). + p = slapcat( suffix=suffix ) + parser = LDIFCallback( module, p.stdout + , partial(wontRemove,module,skipdns) ) + parser.parse() + + changed = True + if module.check_mode: + module.exit_json(changed=changed, msg="remove dir %s" % dbdir) + + # slapd doesn't support database deletion, so we need to turn it + # off and remove it from slapd.d manually. + service( 'slapd', 'stop' ) + path = [ os.sep, 'etc', 'ldap', 'slapd.d' ] + ldif = explode_dn(dn)[::-1] + ldif[-1] += ".ldif" + path.extend( ldif ) + os.unlink( os.path.join(*path) ) + + # delete all children in path, but not the path directory itself. + for file in os.listdir(dbdir): + os.unlink( os.path.join(dbdir, file) ) + service( 'slapd', 'start' ) + return changed + + +# 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' ) + 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') + + 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+' ) + for line in s.readlines(): + if line == '\n': + line = '#\n' + m1 = re1.match(line) + m2 = re2.match(line) + m3 = re3.match(line) + if m1 is not None: + line = 'olcObjectIdentifier: %s' % m1.group(1) + elif m2 is not None: + line = 'olcObjectClasses: %s' % m2.group(1) + elif m3 is not None: + line = 'olcAttributeTypes: %s' % m3.group(1) + + d.write( reSp.sub(line, ' ') ) + + + s.close() + d.close() + return d.name + + +def main(): + module = AnsibleModule( + argument_spec = dict( + dbdirectory = dict( default=None ), + ignoredn = dict( default=None ), + state = dict( default="present", choices=["absent", "present"]), + target = dict( default=None ), + module = dict( default=None ), + suffix = dict( default=None ), + format = dict( default="ldif", choices=["ldif","slapd.conf"] ), + name = dict( default=None ), + ), + supports_check_mode=True + ) + + params = module.params + state = params['state'] + dbdirectory = params['dbdirectory'] + ignoredn = params['ignoredn'] + target = params['target'] + mod = params['module'] + suffix = params['suffix'] + form = params['format'] + name = params['name'] + + if ignoredn is not None: + ignoredn = ignoredn.split(':') + + changed = False + try: + if state == "absent": + if dbdirectory is not None: + changed = removeDB(module,dbdirectory,skipdn=ignoredn) + # TODO: might be useful to be able remove DNs + else: + module.fail_json(msg="missing dbdirectory") + + elif state == "present": + 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: + module.fail_json(msg="missing target or module") + # bind only once per LDIF file for performance + l = ldap.initialize( 'ldapi://' ) + l.sasl_interactive_bind_s('', ldap.sasl.external()) + + if mod is None: + callback = partial(processEntry,module,l) + else: + 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: + module.fail_json(rv=e.returncode, msg=e.output.rstrip()) + except ldap.LDAPError, 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: + module.fail_json(msg=str(e)) + + module.exit_json(changed=changed) + + +# this is magic, see lib/ansible/module_common.py +#<> +main() diff --git a/lib/modules/postmap b/lib/modules/postmap new file mode 100644 index 0000000..7080b25 --- /dev/null +++ b/lib/modules/postmap @@ -0,0 +1,109 @@ +#!/usr/bin/python + +# Create or update postfix's alias and lookup tables +# Copyright (c) 2013 Guilhem Moulin +# +# 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 . + +try: + import selinux + HAVE_SELINUX=True +except ImportError: + HAVE_SELINUX=False + + +# 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' } + 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'] ), + 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: + 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 +#<> +main() diff --git a/lib/modules/postmulti b/lib/modules/postmulti new file mode 100644 index 0000000..d6ecb09 --- /dev/null +++ b/lib/modules/postmulti @@ -0,0 +1,103 @@ +#!/usr/bin/python + +# Create and manage postfix instances. +# Copyright (c) 2013 Guilhem Moulin +# +# 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 . + + +# 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() + + +# 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: + # 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: + module.fail_json(rv=e.returncode, msg=e.output.rstrip()) + + +# this is magic, see lib/ansible/module_common.py +#<> +main() diff --git a/lib/mysql_user b/lib/mysql_user deleted file mode 100644 index 64e1f3d..0000000 --- a/lib/mysql_user +++ /dev/null @@ -1,491 +0,0 @@ -#!/usr/bin/python - -# (c) 2012, Mark Theunissen -# 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 . - -DOCUMENTATION = ''' ---- -module: mysql_user -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 plugin 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 -#<> -main() diff --git a/lib/openldap b/lib/openldap deleted file mode 100644 index 7293b23..0000000 --- a/lib/openldap +++ /dev/null @@ -1,434 +0,0 @@ -#!/usr/bin/python - -# Manage OpenLDAP databases -# Copyright (c) 2013 Guilhem Moulin -# -# 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 . - -import ldap, ldap.sasl -from ldap.filter import filter_format -from ldap.dn import dn2str,explode_dn,str2dn -from ldap.modlist import addModlist -from ldif import LDIFParser -from functools import partial -import re, pwd -import tempfile, atexit - - -# Dirty hack to check equality between the targetted LDIF and that -# currently in the directory. The value of some configuration (olc*) -# attributes is automatically indexed when added; for those we'll add -# explicit indices to what we find in the LDIF. -indexedAttributes = frozenset([ - 'olcAttributeTypes', - 'olcObjectClasses', - 'olcAccess', - 'olcSyncrepl', - 'olcOverlay', - 'olcLimits', -]) - - -# 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 pase, 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')], - 'olcHdbConfig': [('olcDbDirectory', '%s' )], - 'olcOverlayConfig': [('olcOverlay', '%s' )], -} - -# 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\sby\s+dn(?:\.exact)?)= - (?P['\"]?)username=(?P[a-z][-a-z0-9_]*), - (?Pcn=peercred,cn=external,cn=auth) - (?P=quote)\s""" - , re.VERBOSE ) -multispaces = re.compile( r"\s+" ) -pwd_dict = {} - -def acl_sasl_ext(m): - u = m.group('user') - 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') - ) - - -# 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 - - -# Run slapcat(8) on the given suffix or DB number (suffix takes -# precedence) with an optional filter. (This is useful for offline -# searches, or one needs to bypass ACLs.) Returns an open pipe to the -# subprocess. -def slapcat(filter=None, suffix=None, idx=0): - cmd = [ os.path.join(os.sep, 'usr', 'sbin', 'slapcat') ] - - if filter is not None: - cmd.extend([ '-a', filter ]) - - if suffix is not None: - if type(suffix) is not str: - suffix = dn2str(suffix) - cmd.extend([ '-b', suffix ]) - else: - cmd.append( '-n%d' % idx ) - - return subprocess.Popen( cmd, stdout=subprocess.PIPE - , stderr=open(os.devnull, 'wb') ) - - -# Start / stop / whatever a service. -def service(name, state): - cmd = [ os.path.join(os.sep, 'usr', 'sbin', 'service'), name, state ] - subprocess.check_call( cmd, stdout=open(os.devnull, 'wb') - , stderr=subprocess.STDOUT ) - - -# 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]) ) - 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] - 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] ) - - 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'] ) - l.add_s( dn, addModlist(entry) ) - else: - d,e = r - fst = str2dn(dn).pop(0)[0][0] - diff = [] - for a,v in e.iteritems(): - 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] ) - # add explicit indices in the entry from the LDIF - entry[a] = map( (lambda x: '{%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()): - diff.append(( ldap.MOD_ADD, a, entry[a] )) - - if diff: - changed = True - if module.check_mode: - module.exit_json(changed=changed, msg="mod DN %s" % dn) - l.modify_s( d, diff ) - return changed - - -# Load the given module. -def loadModule(module, l, name): - changed = False - - f = filter_format( '(&(objectClass=olcModuleList)(olcModuleLoad=%s))', [name] ) - r = l.search_s( 'cn=config', ldap.SCOPE_ONELEVEL, filterstr = f, attrlist = [''] ) - - if not r: - changed = True - if module.check_mode: - 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 ) - - -# Clear the given DB directory and delete the associated database. Fail -# if non empty, unless all existing DNS are in skipdns. -def wontRemove(module, skipdns, d, _): - if d not in skipdns: - module.fail_json(msg="won't remove '%s'" % d) -def removeDB(module, dbdir, skipdn=None): - changed = False - if not os.path.exists(dbdir): - return False - - l = ldap.initialize( 'ldapi://' ) - l.sasl_interactive_bind_s('', ldap.sasl.external()) - r = getDN_DB( module, l, 'olcDbDirectory', dbdir, attrlist=['olcSuffix'] ) - l.unbind_s() - - if len(r) > 1: - module.fail_json(msg="Multiple results found! This is a bug. Please report.") - elif r: - dn,entry = r.pop() - suffix = entry['olcSuffix'][0] - - skipdns = [suffix] - if skipdn is not None: - skipdns.extend([ "%s,%s" % (s,suffix) for s in skipdn ]) - # here we need to use slapcat not search_s, because we may - # not have read access on the database (even though we're - # root!). - p = slapcat( suffix=suffix ) - parser = LDIFCallback( module, p.stdout - , partial(wontRemove,module,skipdns) ) - parser.parse() - - changed = True - if module.check_mode: - module.exit_json(changed=changed, msg="remove dir %s" % dbdir) - - # slapd doesn't support database deletion, so we need to turn it - # off and remove it from slapd.d manually. - service( 'slapd', 'stop' ) - path = [ os.sep, 'etc', 'ldap', 'slapd.d' ] - ldif = explode_dn(dn)[::-1] - ldif[-1] += ".ldif" - path.extend( ldif ) - os.unlink( os.path.join(*path) ) - - # delete all children in path, but not the path directory itself. - for file in os.listdir(dbdir): - os.unlink( os.path.join(dbdir, file) ) - service( 'slapd', 'start' ) - return changed - - -# 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' ) - 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') - - 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+' ) - for line in s.readlines(): - if line == '\n': - line = '#\n' - m1 = re1.match(line) - m2 = re2.match(line) - m3 = re3.match(line) - if m1 is not None: - line = 'olcObjectIdentifier: %s' % m1.group(1) - elif m2 is not None: - line = 'olcObjectClasses: %s' % m2.group(1) - elif m3 is not None: - line = 'olcAttributeTypes: %s' % m3.group(1) - - d.write( reSp.sub(line, ' ') ) - - - s.close() - d.close() - return d.name - - -def main(): - module = AnsibleModule( - argument_spec = dict( - dbdirectory = dict( default=None ), - ignoredn = dict( default=None ), - state = dict( default="present", choices=["absent", "present"]), - target = dict( default=None ), - module = dict( default=None ), - suffix = dict( default=None ), - format = dict( default="ldif", choices=["ldif","slapd.conf"] ), - name = dict( default=None ), - ), - supports_check_mode=True - ) - - params = module.params - state = params['state'] - dbdirectory = params['dbdirectory'] - ignoredn = params['ignoredn'] - target = params['target'] - mod = params['module'] - suffix = params['suffix'] - form = params['format'] - name = params['name'] - - if ignoredn is not None: - ignoredn = ignoredn.split(':') - - changed = False - try: - if state == "absent": - if dbdirectory is not None: - changed = removeDB(module,dbdirectory,skipdn=ignoredn) - # TODO: might be useful to be able remove DNs - else: - module.fail_json(msg="missing dbdirectory") - - elif state == "present": - 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: - module.fail_json(msg="missing target or module") - # bind only once per LDIF file for performance - l = ldap.initialize( 'ldapi://' ) - l.sasl_interactive_bind_s('', ldap.sasl.external()) - - if mod is None: - callback = partial(processEntry,module,l) - else: - 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: - module.fail_json(rv=e.returncode, msg=e.output.rstrip()) - except ldap.LDAPError, 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: - module.fail_json(msg=str(e)) - - module.exit_json(changed=changed) - - -# this is magic, see lib/ansible/module_common.py -#<> -main() diff --git a/lib/postmap b/lib/postmap deleted file mode 100644 index 7080b25..0000000 --- a/lib/postmap +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/python - -# Create or update postfix's alias and lookup tables -# Copyright (c) 2013 Guilhem Moulin -# -# 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 . - -try: - import selinux - HAVE_SELINUX=True -except ImportError: - HAVE_SELINUX=False - - -# 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' } - 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'] ), - 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: - 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 -#<> -main() diff --git a/lib/postmulti b/lib/postmulti deleted file mode 100644 index d6ecb09..0000000 --- a/lib/postmulti +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/python - -# Create and manage postfix instances. -# Copyright (c) 2013 Guilhem Moulin -# -# 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 . - - -# 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() - - -# 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: - # 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: - module.fail_json(rv=e.returncode, msg=e.output.rstrip()) - - -# this is magic, see lib/ansible/module_common.py -#<> -main() -- cgit v1.2.3