From 19686d549ca13140ff390418e80853b04f477185 Mon Sep 17 00:00:00 2001 From: Dane Summers Date: Wed, 15 Aug 2012 09:04:17 -0400 Subject: [PATCH 01/16] support for subversion repositories --- library/subversion | 146 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 library/subversion diff --git a/library/subversion b/library/subversion new file mode 100644 index 0000000000..3a2456f757 --- /dev/null +++ b/library/subversion @@ -0,0 +1,146 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2012, Michael DeHaan +# +# 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 . + +# I wanted to keep this simple at first, so for now this checks out +# from the given branch of a repo at a particular SHA or +# tag. Latest is not supported, you should not be doing +# that. Contribs welcome! -- MPD + +# requires subversion on the client. + +import re +import logging +logger = logging.getLogger('subversion') +#hdlr = logging.FileHandler('/tmp/subversion.log') +#hdlr.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s')) +#logger.addHandler(hdlr) +logger.setLevel(logging.DEBUG) + +def get_version(dest): + ''' samples the version of the git repo ''' + logger.debug('get_version') + os.chdir(dest) + cmd = "svn info | grep Revision" + logger.debug(cmd) + return os.popen(cmd).read() + +def checkout(repo, dest): + ''' makes a new svn repo if it does not already exist ''' + logger.debug('checkout') + try: + os.makedirs(os.path.dirname(dest)) + except: + pass + cmd = "svn co %s %s" % (repo, dest) + logger.debug(cmd) + cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (out, err) = cmd.communicate() + rc = cmd.returncode + logger.debug('rc, error: %s, %s ' % (rc,err)) + return (rc, out, err) + +def reset(dest): + ''' + throw away any changes? + TODO doesn't seem like a good idea to me... + TODO throw away non-tracked files? + -- svn st | grep '?' | awk '{print $2}' | xargs rm -rf + ''' + logger.debug('reset') + os.chdir(dest) + cmd = "svn revert -R ." + logger.debug(cmd) + cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (out, err) = cmd.communicate() + rc = cmd.returncode + return (rc, out, err) + +def update(module, dest, version): + ''' update an existing svn repo ''' + logger.debug('update') + os.chdir(dest) + cmd = '' + if version != 'HEAD': + cmd = "svn up -r %s" % version + else: + cmd = "svn up" + logger.debug(cmd) + cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (out, err) = cmd.communicate() + rc = cmd.returncode + return (rc, out, err) + +# =========================================== + +def main(): + module = AnsibleModule( + argument_spec = dict( + dest=dict(required=True), + repo=dict(required=True, aliases=['name']), + revision=dict(default='HEAD') + ) + ) + + dest = module.params['dest'] + repo = module.params['repo'] + revision = module.params['revision'] + + rc, out, err, status = (0, None, None, None) + + # if there is no .svn folder, do a checkout + # else update. + before = None + if not os.path.exists("%s/.svn" % (dest)): + logger.debug('.svn exists') + (rc, out, err) = checkout(repo, dest) + if rc != 0: + logger.debug('checkout failure') + module.fail_json(msg=err) + else: + # else do an update + before = get_version(dest) + (rc, out, err) = reset(dest) + if rc != 0: + module.fail_json(msg=err) + + # handle errors from checkout or pull + logger.debug('ERROR: %s' % (err.find('ERROR') != -1)) + if err.find('ERROR') != -1: + logger.debug('err:\n%s' % (err)) + module.fail_json(msg=err) + + # switch to version specified regardless of whether + # we cloned or pulled + (rc, out, err) = update(module, dest, revision) + if rc != 0: + module.fail_json(msg=err) + + # determine if we changed anything + after = get_version(dest) + changed = False + + if before != after: + changed = True + + module.exit_json(changed=changed, before=before, after=after, msg="fell thru the bag") + +# include magic from lib/ansible/module_common.py +#<> +main() From 157fa3868a04b6273bad83795b42acb5e65ed7b7 Mon Sep 17 00:00:00 2001 From: Dane Summers Date: Tue, 21 Aug 2012 13:59:39 -0400 Subject: [PATCH 02/16] added TODO for test scenarios to add --- library/subversion | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/library/subversion b/library/subversion index 3a2456f757..5489089573 100644 --- a/library/subversion +++ b/library/subversion @@ -33,6 +33,21 @@ logger = logging.getLogger('subversion') #logger.addHandler(hdlr) logger.setLevel(logging.DEBUG) +# TODO test scenarios: +# hacking/test-module -m library/subversion ; cat /tmp/subversion.log +# hacking/test-module -m library/subversion -a "repo=\"svn+ssh://pen.syskey.com/opt/subversion/gnconf\"" ; cat /tmp/subversion.log +# hacking/test-module -m library/subversion -a "dest=\"/tmp/gnconf\"" ; cat /tmp/subversion.log +# when /tmp/gnconf doesn't exist: +# hacking/test-module -m library/subversion -a "repo=\"svn+ssh://pen.syskey.com/opt/subversion/gnconf\" dest=\"/tmp/gnconf\"" ; cat /tmp/subversion.log +# when /tmp/gnconf is a folder, but its not an svn repo +# hacking/test-module -m library/subversion -a "repo=\"svn+ssh://pen.syskey.com/opt/subversion/gnconf\" dest=\"/tmp/gnconf\"" ; cat /tmp/subversion.log +# when /tmp/gnconf is a folder, but its a file (not a folder) +# hacking/test-module -m library/subversion -a "repo=\"svn+ssh://pen.syskey.com/opt/subversion/gnconf\" dest=\"/tmp/gnconf\"" ; cat /tmp/subversion.log +# when /tmp/gnconf is a folder, when its a different svn URL +# hacking/test-module -m library/subversion -a "repo=\"svn+ssh://pen.syskey.com/opt/subversion/gnconf\" dest=\"/tmp/gnconf\"" ; cat /tmp/subversion.log +# when /tmp/gnconf is a folder, when its a different revision +# hacking/test-module -m library/subversion -a "repo=\"svn+ssh://pen.syskey.com/opt/subversion/gnconf\" dest=\"/tmp/gnconf\"" ; cat /tmp/subversion.log + def get_version(dest): ''' samples the version of the git repo ''' logger.debug('get_version') From 24c8c22e754cd78af1aff57e441960ae091dafc5 Mon Sep 17 00:00:00 2001 From: Dane Summers Date: Tue, 21 Aug 2012 16:10:17 -0400 Subject: [PATCH 03/16] removed logger, removed superfluous mkdir --- library/subversion | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/library/subversion b/library/subversion index 5489089573..59292d5f78 100644 --- a/library/subversion +++ b/library/subversion @@ -26,12 +26,6 @@ # requires subversion on the client. import re -import logging -logger = logging.getLogger('subversion') -#hdlr = logging.FileHandler('/tmp/subversion.log') -#hdlr.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s')) -#logger.addHandler(hdlr) -logger.setLevel(logging.DEBUG) # TODO test scenarios: # hacking/test-module -m library/subversion ; cat /tmp/subversion.log @@ -50,25 +44,16 @@ logger.setLevel(logging.DEBUG) def get_version(dest): ''' samples the version of the git repo ''' - logger.debug('get_version') os.chdir(dest) cmd = "svn info | grep Revision" - logger.debug(cmd) return os.popen(cmd).read() def checkout(repo, dest): ''' makes a new svn repo if it does not already exist ''' - logger.debug('checkout') - try: - os.makedirs(os.path.dirname(dest)) - except: - pass cmd = "svn co %s %s" % (repo, dest) - logger.debug(cmd) cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = cmd.communicate() rc = cmd.returncode - logger.debug('rc, error: %s, %s ' % (rc,err)) return (rc, out, err) def reset(dest): @@ -78,10 +63,8 @@ def reset(dest): TODO throw away non-tracked files? -- svn st | grep '?' | awk '{print $2}' | xargs rm -rf ''' - logger.debug('reset') os.chdir(dest) cmd = "svn revert -R ." - logger.debug(cmd) cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = cmd.communicate() rc = cmd.returncode @@ -89,14 +72,12 @@ def reset(dest): def update(module, dest, version): ''' update an existing svn repo ''' - logger.debug('update') os.chdir(dest) cmd = '' if version != 'HEAD': cmd = "svn up -r %s" % version else: cmd = "svn up" - logger.debug(cmd) cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = cmd.communicate() rc = cmd.returncode @@ -123,10 +104,8 @@ def main(): # else update. before = None if not os.path.exists("%s/.svn" % (dest)): - logger.debug('.svn exists') (rc, out, err) = checkout(repo, dest) if rc != 0: - logger.debug('checkout failure') module.fail_json(msg=err) else: # else do an update @@ -136,9 +115,7 @@ def main(): module.fail_json(msg=err) # handle errors from checkout or pull - logger.debug('ERROR: %s' % (err.find('ERROR') != -1)) if err.find('ERROR') != -1: - logger.debug('err:\n%s' % (err)) module.fail_json(msg=err) # switch to version specified regardless of whether @@ -154,7 +131,7 @@ def main(): if before != after: changed = True - module.exit_json(changed=changed, before=before, after=after, msg="fell thru the bag") + module.exit_json(changed=changed, before=before, after=after, msg="") # include magic from lib/ansible/module_common.py #<> From 4e833cf50668e9fbb298c605922d6bfda66ef484 Mon Sep 17 00:00:00 2001 From: Pepe Barbe Date: Tue, 21 Aug 2012 11:20:16 -0500 Subject: [PATCH 04/16] Initial commit of change of semantics for module The postgresql_user module has several drawbacks: * No granularity for privileges * PostgreSQL semantics force working on one database at time, at least for Tables. Which means that a single call can't remove all the privileges for a user, and a user can't be removed until all the privileges are removed, forcing a module failure with no way to work around the issue. Changes: * Added the ability to specify granular privileges for database and tables within the database * Report if user was removed, and add an option to disable failing if user is not removed. --- library/postgresql_user | 191 ++++++++++++++++++++++++++++++---------- 1 file changed, 143 insertions(+), 48 deletions(-) diff --git a/library/postgresql_user b/library/postgresql_user index 56b0abd58e..51d971db96 100755 --- a/library/postgresql_user +++ b/library/postgresql_user @@ -34,35 +34,14 @@ def user_exists(cursor, user): return cursor.rowcount > 0 -def user_add(cursor, user, password, db): +def user_add(cursor, user, password): """Create a new user with write access to the database""" query = "CREATE USER %(user)s with PASSWORD '%(password)s'" cursor.execute(query % {"user": user, "password": password}) - grant_privileges(cursor, user, db) return True - -def has_privileges(cursor, user, db): - """Check if the user has create privileges on the database""" - query = "SELECT has_database_privilege(%(user)s, %(db)s, 'CREATE')" - cursor.execute(query, {'user': user, 'db': db}) - return cursor.fetchone()[0] - - -def grant_privileges(cursor, user, db): - """Grant all privileges on the database""" - query = "GRANT ALL PRIVILEGES ON DATABASE %(db)s TO %(user)s" - cursor.execute(query % {'user': user, 'db': db}) - - -def revoke_privileges(cursor, user, db): - """Revoke all privileges on the database""" - query = "REVOKE ALL PRIVILEGES ON DATABASE %(db)s FROM %(user)s" - cursor.execute(query % {'user': user, 'db': db}) - - -def user_mod(cursor, user, password, db): - """Update password and permissions""" +def user_chpass(cursor, user, password): + """Change user password""" changed = False # Handle passwords. @@ -79,28 +58,131 @@ def user_mod(cursor, user, password, db): if current_pass_hash != new_pass_hash: changed = True - # Handle privileges. - # For now, we just check if the user has access to the database - if not has_privileges(cursor, user, db): - grant_privileges(cursor, user, db) - changed = True - return changed +def user_delete(cursor, user): + """Try to remove a user. Returns True if successful otherwise False""" + cursor.execute("SAVEPOINT ansible_pgsql_user_delete") + try: + cursor.execute("DROP USER %s" % user) + except: + cursor.execute("ROLLBACK TO SAVEPOINT ansible_pgsql_user_delete") + cursor.execute("RELEASE SAVEPOINT ansible_pgsql_user_delete") + return False + + cursor.execute("RELEASE SAVEPOINT ansible_pgsql_user_delete") + return True -def user_delete(cursor, user, db): - """Delete a user, first revoking privileges""" - revoke_privileges(cursor, user, db) - cursor.execute("DROP USER %(user)s" % {'user': user}) +def has_table_privilege(cursor, user, table, priv): + query = 'SELECT has_table_privilege(%s, %s, %s)' + cursor.execute(query, user, table, priv) + return cursor.fetchone()[0] + +def grant_table_privilege(cursor, user, table, priv): + if has_table_privilege(cursor, user, table, priv): + return False + query = 'GRANT %s ON TABLE %s TO %s' % (priv, table, user) + cursor.execute(query) + return True + +def revoke_table_privilege(cursor, user, table, priv): + if not has_table_privilege(cursor, user, table, priv): + return False + query = 'REVOKE %s ON TABLE %s FROM %s' % (priv, table, user) + cursor.execute(query) return True +def has_database_privilege(cursor, user, db, priv): + query = 'SELECT has_database_privilege(%s, %s, %s)' + cursor.execute(query, user, db, priv) + return cursor.fetchone()[0] + +def grant_database_privilege(cursor, user, db, priv): + if has_database_privilege(cursor, user, db, priv): + return False + query = 'GRANT %s ON DATABASE %s TO %s' % (priv, db, user) + cursor.execute(query) + return True + +def revoke_database_privilege(cursor, user, db, priv): + if not has_database_privilege(cursor, user, db, priv): + return False + query = 'REVOKE %s ON DATABASE %s FROM %s' % (priv, db, user) + cursor.execute(query) + return True + +def revoke_privileges(cursor, user, privs): + if privs is None: + return False + + changed = False + for type_ in privs: + revoke_func = { + 'table':revoke_table_privilege, + 'database':revoke_database_privilege + }[type_] + for name, privileges in privs[type_].iteritem(): + for privilege in privileges: + changed = revoke_func(cursor, user, name, privilege)\ + or changed + + return changed + +def grant_privileges(cursor, user, privs): + if privs is None: + return False + + changed = False + for type_ in privs: + grant_func = { + 'table':grant_table_privilege, + 'database':grant_database_privilege + }[type_] + for name, privileges in privs[type_].iteritem(): + for privilege in privileges: + changed = grant_func(cursor, user, name, privilege)\ + or changed + + return changed + +def parse_privs(privs, db): + """ + Parse privilege string to determine permissions for database db. + Format: + + privileges[/privileges/...] + + Where: + + privileges := DATABASE_PRIVILEGES[,DATABASE_PRIVILEGES,...] | + TABLE_NAME:TABLE_PRIVILEGES[,TABLE_PRIVILEGES,...] + """ + if privs is None: + return privs + + privs = { + 'database':{}, + 'table':{} + } + for token in privs.split('/'): + if ':' not in token: + type_ = 'database' + name = db + privileges = token + else: + type_ = 'table' + name, privileges = token.split(':', 1) + privileges = privileges.split(',') + + privs[type_][name] = privileges + + return privs # =========================================== # Module execution. # - def main(): module = AnsibleModule( argument_spec=dict( @@ -110,13 +192,19 @@ def main(): user=dict(required=True, aliases=['name']), password=dict(default=None), state=dict(default="present", choices=["absent", "present"]), - db=dict(required=True), + priv=dict(default=None), + db=dict(default=''), + fail_on_user=dict(default=True) ) ) user = module.params["user"] password = module.params["password"] state = module.params["state"] + fail_on_user = module.params["fail_on_user"] db = module.params["db"] + if db == '' and module.params["priv"] is not None: + module.fail_json(msg="privileges require a database to be specified") + privs = parse_privs(module.params["priv"], db) if not postgresqldb_found: module.fail_json(msg="the python psycopg2 module is required") @@ -127,7 +215,8 @@ def main(): params_map = { "login_host":"host", "login_user":"user", - "login_password":"password" + "login_password":"password", + "db":"database" } kw = dict( (params_map[k], v) for (k, v) in module.params.iteritems() if k in params_map and v != "" ) @@ -136,24 +225,30 @@ def main(): cursor = db_connection.cursor() except Exception, e: module.fail_json(msg="unable to connect to database: %s" % e) - + + changed = False if state == "present": if user_exists(cursor, user): - changed = user_mod(cursor, user, password, db) + changed = user_chpass(cursor, user, password) else: if password is None: msg = "password parameter required when adding a user" module.fail_json(msg=msg) - changed = user_add(cursor, user, password, db) - - elif state == "absent": + changed = user_add(cursor, user, password) + changed = grant_privileges(cursor, user, privs) or changed + else: if user_exists(cursor, user): - changed = user_delete(cursor, user, db) - else: - changed = False - # Commit the database changes - db_connection.commit() - module.exit_json(changed=changed, user=user) + changed = revoke_privileges(cursor, user, privs) + user_removed = user_delete(cursor, user) + changed = changed or user_removed + + if fail_on_user and not user_removed: + msg = "unabel to remove user" + module.fail_json(msg=msg) + + if changed: + db_connection.commit() + module.exit_json(changed=changed, user=user, user_removed=user_removed) # this is magic, see lib/ansible/module_common.py #<> From 95169b75c463a852587c404ae8551216e282c7fc Mon Sep 17 00:00:00 2001 From: Pepe Barbe Date: Tue, 21 Aug 2012 14:23:45 -0500 Subject: [PATCH 05/16] Add fail_on_user option fail_on_user option can be used to ignore silently if the user cannot be removed because of remaining privilege dependencies to other objects in the database. By default it will fail, so that this new behavior won't surprise unsuspecting users. --- library/postgresql_user | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/library/postgresql_user b/library/postgresql_user index 51d971db96..dbef34e0ec 100755 --- a/library/postgresql_user +++ b/library/postgresql_user @@ -194,13 +194,13 @@ def main(): state=dict(default="present", choices=["absent", "present"]), priv=dict(default=None), db=dict(default=''), - fail_on_user=dict(default=True) + fail_on_user=dict(default='yes') ) ) user = module.params["user"] password = module.params["password"] state = module.params["state"] - fail_on_user = module.params["fail_on_user"] + fail_on_user = module.params["fail_on_user"] == 'yes' db = module.params["db"] if db == '' and module.params["priv"] is not None: module.fail_json(msg="privileges require a database to be specified") @@ -221,12 +221,14 @@ def main(): kw = dict( (params_map[k], v) for (k, v) in module.params.iteritems() if k in params_map and v != "" ) try: - db_connection = psycopg2.connect(database=db, **kw) + db_connection = psycopg2.connect(**kw) cursor = db_connection.cursor() except Exception, e: module.fail_json(msg="unable to connect to database: %s" % e) + kw = dict(user=user) changed = False + user_removed = False if state == "present": if user_exists(cursor, user): changed = user_chpass(cursor, user, password) @@ -241,14 +243,16 @@ def main(): changed = revoke_privileges(cursor, user, privs) user_removed = user_delete(cursor, user) changed = changed or user_removed - - if fail_on_user and not user_removed: - msg = "unabel to remove user" - module.fail_json(msg=msg) + if fail_on_user and not user_removed: + msg = "unabel to remove user" + module.fail_json(msg=msg) + kw['user_removed'] = user_removed if changed: db_connection.commit() - module.exit_json(changed=changed, user=user, user_removed=user_removed) + + kw['changed'] = changed + module.exit_json(**kw) # this is magic, see lib/ansible/module_common.py #<> From af5d67c496c278dd8b8f4bc0af39a8ba64a504d3 Mon Sep 17 00:00:00 2001 From: Pepe Barbe Date: Tue, 21 Aug 2012 14:25:19 -0500 Subject: [PATCH 06/16] Query for all active privileges instead Use a different method to query for current privileges at the table and database level. This method is more robust if newer privileges are added in future versions and also supports the ALL wildcard. --- library/postgresql_user | 72 +++++++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/library/postgresql_user b/library/postgresql_user index dbef34e0ec..8bde46aedc 100755 --- a/library/postgresql_user +++ b/library/postgresql_user @@ -16,6 +16,8 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +import re + try: import psycopg2 except ImportError: @@ -75,42 +77,70 @@ def user_delete(cursor, user): def has_table_privilege(cursor, user, table, priv): query = 'SELECT has_table_privilege(%s, %s, %s)' - cursor.execute(query, user, table, priv) + cursor.execute(query, (user, table, priv)) return cursor.fetchone()[0] +def get_table_privileges(cursor, user, table): + if '.' in table: + schema, table = table.split('.', 1) + else: + schema = 'public' + query = '''SELECT privilege_type FROM information_schema.role_table_grants + WHERE grantee=%s AND table_name=%s AND table_schema=%s''' + cursor.execute(query, (user, table, schema)) + return set([x[0] for x in cursor.fetchall()]) + + def grant_table_privilege(cursor, user, table, priv): - if has_table_privilege(cursor, user, table, priv): - return False + prev_priv = get_table_privileges(cursor, user, table) query = 'GRANT %s ON TABLE %s TO %s' % (priv, table, user) cursor.execute(query) - return True + curr_priv = get_table_privileges(cursor, user, table) + return len(curr_priv) > len(prev_priv) def revoke_table_privilege(cursor, user, table, priv): - if not has_table_privilege(cursor, user, table, priv): - return False + prev_priv = get_table_privileges(cursor, user, table) query = 'REVOKE %s ON TABLE %s FROM %s' % (priv, table, user) cursor.execute(query) - return True + curr_priv = get_table_privileges(cursor, user, table) + return len(curr_priv) < len(prev_priv) +def get_database_privileges(cursor, user, db): + priv_map = { + 'C':'CREATE', + 'T':'TEMPORARY', + 'c':'CONNECT', + } + query = 'SELECT datacl FROM pg_database WHERE datname = %s' + cursor.execute(query, (db,)) + datacl = cursor.fetchone()[0] + r = re.search('%s=(C?T?c?)/[a-z]+\,?' % user, datacl) + if r is None: + return [] + o = [] + for v in r.group(1): + o.append(priv_map[v]) + return o + def has_database_privilege(cursor, user, db, priv): query = 'SELECT has_database_privilege(%s, %s, %s)' - cursor.execute(query, user, db, priv) + cursor.execute(query, (user, db, priv)) return cursor.fetchone()[0] def grant_database_privilege(cursor, user, db, priv): - if has_database_privilege(cursor, user, db, priv): - return False + prev_priv = get_database_privileges(cursor, user, db) query = 'GRANT %s ON DATABASE %s TO %s' % (priv, db, user) cursor.execute(query) - return True + curr_priv = get_database_privileges(cursor, user, db) + return len(curr_priv) > len(prev_priv) def revoke_database_privilege(cursor, user, db, priv): - if not has_database_privilege(cursor, user, db, priv): - return False + prev_priv = get_database_privileges(cursor, user, db) query = 'REVOKE %s ON DATABASE %s FROM %s' % (priv, db, user) cursor.execute(query) - return True + curr_priv = get_database_privileges(cursor, user, db) + return len(curr_priv) < len(prev_priv) def revoke_privileges(cursor, user, privs): if privs is None: @@ -122,7 +152,7 @@ def revoke_privileges(cursor, user, privs): 'table':revoke_table_privilege, 'database':revoke_database_privilege }[type_] - for name, privileges in privs[type_].iteritem(): + for name, privileges in privs[type_].iteritems(): for privilege in privileges: changed = revoke_func(cursor, user, name, privilege)\ or changed @@ -139,7 +169,7 @@ def grant_privileges(cursor, user, privs): 'table':grant_table_privilege, 'database':grant_database_privilege }[type_] - for name, privileges in privs[type_].iteritem(): + for name, privileges in privs[type_].iteritems(): for privilege in privileges: changed = grant_func(cursor, user, name, privilege)\ or changed @@ -161,7 +191,7 @@ def parse_privs(privs, db): if privs is None: return privs - privs = { + o_privs = { 'database':{}, 'table':{} } @@ -169,15 +199,15 @@ def parse_privs(privs, db): if ':' not in token: type_ = 'database' name = db - privileges = token + priv_set = set(x.strip() for x in token.split(',')) else: type_ = 'table' name, privileges = token.split(':', 1) - privileges = privileges.split(',') + priv_set = set(x.strip() for x in privileges.split(',')) - privs[type_][name] = privileges + o_privs[type_][name] = priv_set - return privs + return o_privs # =========================================== # Module execution. From 6d473df324ff63b665cd8050cd7053713f78a64a Mon Sep 17 00:00:00 2001 From: Pepe Barbe Date: Wed, 22 Aug 2012 12:19:55 -0500 Subject: [PATCH 07/16] Typo --- library/postgresql_user | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/postgresql_user b/library/postgresql_user index 8bde46aedc..d883cc57cc 100755 --- a/library/postgresql_user +++ b/library/postgresql_user @@ -274,7 +274,7 @@ def main(): user_removed = user_delete(cursor, user) changed = changed or user_removed if fail_on_user and not user_removed: - msg = "unabel to remove user" + msg = "unable to remove user" module.fail_json(msg=msg) kw['user_removed'] = user_removed From fdbc99dc28686883a988f7eed0a0b19366f5a4bd Mon Sep 17 00:00:00 2001 From: Pepe Barbe Date: Wed, 22 Aug 2012 12:20:51 -0500 Subject: [PATCH 08/16] Check for database ownership --- library/postgresql_db | 53 ++++++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/library/postgresql_db b/library/postgresql_db index 8bfdc9fd20..ca5e805318 100755 --- a/library/postgresql_db +++ b/library/postgresql_db @@ -27,26 +27,45 @@ else: # PostgreSQL module specific support methods. # +def set_owner(cursor, db, owner): + query = "ALTER DATABASE %s OWNER TO %s" % (db, owner) + cursor.execute(query) + return True + +def db_owned_by(cursor, db, user): + query = """SELECT count(*) FROM pg_database JOIN pg_user ON datdba = usesysid + WHERE usename = %(user)s and datname = %(db)s""" + cursor.execute(query, {'db':db, 'user':user}) + return cursor.rowcount == 1 + def db_exists(cursor, db): query = "SELECT * FROM pg_database WHERE datname=%(db)s" cursor.execute(query, {'db': db}) return cursor.rowcount == 1 def db_delete(cursor, db): - query = "DROP DATABASE %s" % db - cursor.execute(query) - return True + if db_exists(cursor, db): + query = "DROP DATABASE %s" % db + cursor.execute(query) + return True + else: + return False def db_create(cursor, db, owner, template, encoding): - if owner: - owner = " OWNER %s" % owner - if template: - template = " TEMPLATE %s" % template - if encoding: - encoding = " ENCODING '%s'" % encoding - query = "CREATE DATABASE %s%s%s%s" % (db, owner, template, encoding) - cursor.execute(query) - return True + if not db_exists(cursor, db): + if owner: + owner = " OWNER %s" % owner + if template: + template = " TEMPLATE %s" % template + if encoding: + encoding = " ENCODING '%s'" % encoding + query = "CREATE DATABASE %s%s%s%s" % (db, owner, template, encoding) + cursor.execute(query) + return True + elif owner and not db_owned_by(cursor, db, owner): + return set_owner(cursor, db, owner) + else: + return False # =========================================== # Module execution. @@ -100,12 +119,10 @@ def main(): module.fail_json(msg="unable to connect to database: %s" % e) try: - if db_exists(cursor, db): - if state == "absent": - changed = db_delete(cursor, db) - else: - if state == "present": - changed = db_create(cursor, db, owner, template, encoding) + if state == "absent": + changed = db_delete(cursor, db) + elif state == "present": + changed = db_create(cursor, db, owner, template, encoding) except Exception, e: module.fail_json(msg="Database query failed: %s" % e) From fdaf65282b4cd7d76758bc6ed4f6feda270fedd0 Mon Sep 17 00:00:00 2001 From: Pepe Barbe Date: Wed, 22 Aug 2012 13:34:24 -0500 Subject: [PATCH 09/16] bugfix in sql query --- library/postgresql_db | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/postgresql_db b/library/postgresql_db index ca5e805318..32ee920590 100755 --- a/library/postgresql_db +++ b/library/postgresql_db @@ -33,7 +33,7 @@ def set_owner(cursor, db, owner): return True def db_owned_by(cursor, db, user): - query = """SELECT count(*) FROM pg_database JOIN pg_user ON datdba = usesysid + query = """SELECT * FROM pg_database JOIN pg_user ON datdba = usesysid WHERE usename = %(user)s and datname = %(db)s""" cursor.execute(query, {'db':db, 'user':user}) return cursor.rowcount == 1 From 5c4a5231480fc7bb70b3fd639a75c242a24d364f Mon Sep 17 00:00:00 2001 From: Pepe Barbe Date: Wed, 22 Aug 2012 13:54:25 -0500 Subject: [PATCH 10/16] fix _gitinfo function to avoid ansible crashing in cases where the branch file is missing (e.g. after a gc) --- lib/ansible/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/ansible/utils.py b/lib/ansible/utils.py index 2b21ac0ec6..5a0fb46b5c 100644 --- a/lib/ansible/utils.py +++ b/lib/ansible/utils.py @@ -317,6 +317,7 @@ def _gitinfo(): branch = f.readline().split('/')[-1].rstrip("\n") f.close() branch_path = os.path.join(repo_path, "refs", "heads", branch) + if os.path.exists(branch_path): f = open(branch_path) commit = f.readline()[:10] f.close() @@ -327,6 +328,8 @@ def _gitinfo(): offset = time.altzone result = "({0} {1}) last updated {2} (GMT {3:+04d})".format(branch, commit, time.strftime("%Y/%m/%d %H:%M:%S", date), offset / -36) + else: + result = 'n/a' return result def version(prog): From 6dd6a4c534f428ae0d1c571048a5f61cf157f42d Mon Sep 17 00:00:00 2001 From: Dane Summers Date: Wed, 22 Aug 2012 23:06:03 -0400 Subject: [PATCH 11/16] tested library - fixed several test cases, added 'force' option, and removed grep requirement --- library/subversion | 89 +++++++++++++++++++++++++++------------------- 1 file changed, 52 insertions(+), 37 deletions(-) diff --git a/library/subversion b/library/subversion index 59292d5f78..8dd2e3160e 100644 --- a/library/subversion +++ b/library/subversion @@ -23,30 +23,17 @@ # tag. Latest is not supported, you should not be doing # that. Contribs welcome! -- MPD -# requires subversion on the client. +# requires subversion and grep on the client. import re -# TODO test scenarios: -# hacking/test-module -m library/subversion ; cat /tmp/subversion.log -# hacking/test-module -m library/subversion -a "repo=\"svn+ssh://pen.syskey.com/opt/subversion/gnconf\"" ; cat /tmp/subversion.log -# hacking/test-module -m library/subversion -a "dest=\"/tmp/gnconf\"" ; cat /tmp/subversion.log -# when /tmp/gnconf doesn't exist: -# hacking/test-module -m library/subversion -a "repo=\"svn+ssh://pen.syskey.com/opt/subversion/gnconf\" dest=\"/tmp/gnconf\"" ; cat /tmp/subversion.log -# when /tmp/gnconf is a folder, but its not an svn repo -# hacking/test-module -m library/subversion -a "repo=\"svn+ssh://pen.syskey.com/opt/subversion/gnconf\" dest=\"/tmp/gnconf\"" ; cat /tmp/subversion.log -# when /tmp/gnconf is a folder, but its a file (not a folder) -# hacking/test-module -m library/subversion -a "repo=\"svn+ssh://pen.syskey.com/opt/subversion/gnconf\" dest=\"/tmp/gnconf\"" ; cat /tmp/subversion.log -# when /tmp/gnconf is a folder, when its a different svn URL -# hacking/test-module -m library/subversion -a "repo=\"svn+ssh://pen.syskey.com/opt/subversion/gnconf\" dest=\"/tmp/gnconf\"" ; cat /tmp/subversion.log -# when /tmp/gnconf is a folder, when its a different revision -# hacking/test-module -m library/subversion -a "repo=\"svn+ssh://pen.syskey.com/opt/subversion/gnconf\" dest=\"/tmp/gnconf\"" ; cat /tmp/subversion.log - def get_version(dest): ''' samples the version of the git repo ''' os.chdir(dest) - cmd = "svn info | grep Revision" - return os.popen(cmd).read() + cmd = "svn info" + revision = filter(lambda l: re.search('Revision',l) != None,os.popen(cmd).read().splitlines()) + url = filter(lambda l: re.search('^URL',l) != None,os.popen(cmd).read().splitlines()) + return [revision[0],url[0]] def checkout(repo, dest): ''' makes a new svn repo if it does not already exist ''' @@ -56,20 +43,38 @@ def checkout(repo, dest): rc = cmd.returncode return (rc, out, err) -def reset(dest): - ''' - throw away any changes? - TODO doesn't seem like a good idea to me... - TODO throw away non-tracked files? - -- svn st | grep '?' | awk '{print $2}' | xargs rm -rf - ''' - os.chdir(dest) - cmd = "svn revert -R ." +def switch(repo, dest): + ''' makes a new svn repo if it does not already exist ''' + cmd = "svn sw %s %s" % (repo, dest) cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = cmd.communicate() rc = cmd.returncode return (rc, out, err) +def has_local_mods(dest): + os.chdir(dest) + cmd = "svn status" + words = os.popen(cmd).read() + splitup = words.splitlines() + filtered = filter(lambda c: re.search('^\\?.*$',c) == None,splitup) + return len(filtered) > 0 + +def reset(dest,force): + ''' + Reset the repo: + force: if true, then remove any local modifications. Else, fail if there are local modifications + ''' + if has_local_mods(dest): + if force: + cmd = "svn revert -R ." + cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (out, err) = cmd.communicate() + rc = cmd.returncode + return (rc, out, err) + else: + return (-1,"ERROR: modified files exist in the repository.","") + return (0,"","") + def update(module, dest, version): ''' update an existing svn repo ''' os.chdir(dest) @@ -90,31 +95,41 @@ def main(): argument_spec = dict( dest=dict(required=True), repo=dict(required=True, aliases=['name']), - revision=dict(default='HEAD') + revision=dict(default='HEAD'), + force=dict(default='no', choices=['yes', 'no'], aliases=['force']) ) ) - dest = module.params['dest'] - repo = module.params['repo'] + dest = module.params['dest'] + repo = module.params['repo'] revision = module.params['revision'] + force = module.boolean(module.params['force']) rc, out, err, status = (0, None, None, None) # if there is no .svn folder, do a checkout # else update. before = None + local_mods = False if not os.path.exists("%s/.svn" % (dest)): - (rc, out, err) = checkout(repo, dest) - if rc != 0: - module.fail_json(msg=err) + if os.path.exists(dest): + module.fail_json(msg="%s folder already exists, but its not a subversion repository." % (dest)) + else: + (rc, out, err) = checkout(repo, dest) + if rc != 0: + module.fail_json(msg=err) else: + local_mods = has_local_mods(dest) # else do an update before = get_version(dest) - (rc, out, err) = reset(dest) + (rc, out, err) = reset(dest,force) + if rc != 0: + module.fail_json(msg=err) + (rc, out, err) = switch(repo, dest) if rc != 0: module.fail_json(msg=err) - # handle errors from checkout or pull + # handle errors from switch or pull if err.find('ERROR') != -1: module.fail_json(msg=err) @@ -128,10 +143,10 @@ def main(): after = get_version(dest) changed = False - if before != after: + if before != after or local_mods: changed = True - module.exit_json(changed=changed, before=before, after=after, msg="") + module.exit_json(changed=changed, before=before, after=after) # include magic from lib/ansible/module_common.py #<> From 71cff252549f600b614b2ddcbb1dfe577e4bf7f2 Mon Sep 17 00:00:00 2001 From: Dane Summers Date: Thu, 23 Aug 2012 00:07:14 -0400 Subject: [PATCH 12/16] added force option to git - made both subversion and git default to force=true for backward compatibility with git's previous behavior --- library/git | 22 ++++++++++++++++++---- library/subversion | 7 +++---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/library/git b/library/git index cc8f7e41a2..1bb5c94b0d 100755 --- a/library/git +++ b/library/git @@ -45,13 +45,23 @@ def clone(repo, dest): rc = cmd.returncode return (rc, out, err) -def reset(dest): + +def has_local_mods(dest): + os.chdir(dest) + cmd = "git status -s" + lines = os.popen(cmd).read().splitlines() + lines = filter(lambda c: re.search('^\\?\\?.*$',c) == None,lines) + return len(lines) > 0 + +def reset(module,dest,force): ''' Resets the index and working tree to HEAD. Discards any changes to tracked files in working tree since that commit. ''' os.chdir(dest) + if not force and has_local_mods(dest): + module.fail_json(msg="Local modifications exist in repository (force=no).") cmd = "git reset --hard HEAD" cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = cmd.communicate() @@ -140,7 +150,8 @@ def main(): dest=dict(required=True), repo=dict(required=True, aliases=['name']), version=dict(default='HEAD'), - remote=dict(default='origin') + remote=dict(default='origin'), + force=dict(default='yes', choices=['yes', 'no'], aliases=['force']) ) ) @@ -148,6 +159,7 @@ def main(): repo = module.params['repo'] version = module.params['version'] remote = module.params['remote'] + force = module.boolean(module.params['force']) gitconfig = os.path.join(dest, '.git', 'config') @@ -156,14 +168,16 @@ def main(): # if there is no git configuration, do a clone operation # else pull and switch the version before = None + local_mods = False if not os.path.exists(gitconfig): (rc, out, err) = clone(repo, dest) if rc != 0: module.fail_json(msg=err) else: # else do a pull + local_mods = has_local_mods(dest) before = get_version(dest) - (rc, out, err) = reset(dest) + (rc, out, err) = reset(module,dest,force) if rc != 0: module.fail_json(msg=err) (rc, out, err) = pull(module, repo, dest, version) @@ -182,7 +196,7 @@ def main(): after = get_version(dest) changed = False - if before != after: + if before != after or local_mods: changed = True module.exit_json(changed=changed, before=before, after=after) diff --git a/library/subversion b/library/subversion index 8dd2e3160e..c8782216f5 100644 --- a/library/subversion +++ b/library/subversion @@ -54,9 +54,8 @@ def switch(repo, dest): def has_local_mods(dest): os.chdir(dest) cmd = "svn status" - words = os.popen(cmd).read() - splitup = words.splitlines() - filtered = filter(lambda c: re.search('^\\?.*$',c) == None,splitup) + lines = os.popen(cmd).read().splitlines() + filtered = filter(lambda c: re.search('^\\?.*$',c) == None,lines) return len(filtered) > 0 def reset(dest,force): @@ -96,7 +95,7 @@ def main(): dest=dict(required=True), repo=dict(required=True, aliases=['name']), revision=dict(default='HEAD'), - force=dict(default='no', choices=['yes', 'no'], aliases=['force']) + force=dict(default='yes', choices=['yes', 'no'], aliases=['force']) ) ) From eb49ee38aeb84536daa89248edb4f3dfb7e49e7b Mon Sep 17 00:00:00 2001 From: Dane Summers Date: Thu, 23 Aug 2012 00:08:38 -0400 Subject: [PATCH 13/16] full test case for subversion, minimal tests for git --- test/TestRunner.py | 81 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/test/TestRunner.py b/test/TestRunner.py index 16c962ad70..17dcf5b9fc 100644 --- a/test/TestRunner.py +++ b/test/TestRunner.py @@ -138,6 +138,85 @@ class TestRunner(unittest.TestCase): assert 'failed' not in result assert result['rc'] == 0 + def test_git(self): + self._run('file',['path=/tmp/gitdemo','state=absent']) + self._run('file',['path=/tmp/gd','state=absent']) + self._run('command',['git init gitdemo', 'chdir=/tmp']) + self._run('command',['touch a', 'chdir=/tmp/gitdemo']) + self._run('command',['git add *', 'chdir=/tmp/gitdemo']) + self._run('command',['git commit -m "test commit 2"', 'chdir=/tmp/gitdemo']) + self._run('command',['touch b', 'chdir=/tmp/gitdemo']) + self._run('command',['git add *', 'chdir=/tmp/gitdemo']) + self._run('command',['git commit -m "test commit 2"', 'chdir=/tmp/gitdemo']) + result = self._run('git', [ "repo=\"file:///tmp/gitdemo\"", "dest=/tmp/gd" ]) + assert result['changed'] + # test the force option not set + self._run('file',['path=/tmp/gd/a','state=absent']) + result = self._run('git', [ "repo=\"file:///tmp/gitdemo\"", "dest=/tmp/gd", "force=no" ]) + assert result['failed'] + # test the force option when set + result = self._run('git', [ "repo=\"file:///tmp/gitdemo\"", "dest=/tmp/gd", "force=yes" ]) + assert result['changed'] + + def test_subversion(self): + # TODO make an svn repo locally so as to avoid tests failing on network calls + self._run('file',['path=/tmp/meetings','state=absent']) + # hacking/test-module -m library/subversion + result = self._run('subversion', [ ]) + assert result['failed'] + assert "dest" in result['msg'] + assert "repo" in result['msg'] + # hacking/test-module -m library/subversion -a "repo=\"http://svn.apache.org/repos/asf/subversion/trunk/notes/meetings\"" + result = self._run('subversion', [ "repo=\"http://svn.apache.org/repos/asf/subversion/trunk/notes/meetings\"" ]) + assert result['failed'] + assert "dest" in result['msg'] + # hacking/test-module -m library/subversion -a "dest=\"/tmp/gnconf\"" + result = self._run('subversion', [ "dest=\"/tmp/gnconf\"" ]) + assert result['failed'] + assert "repo" in result['msg'] + # when /tmp/meetings doesn't exist: + # hacking/test-module -m library/subversion -a "repo=\"repo\" dest=\"/tmp/gnconf\"" + result = self._run('subversion', [ "repo=\"http://svn.apache.org/repos/asf/subversion/trunk/notes/meetings\"","dest=\"/tmp/meetings\"" ]) + assert result['changed'] + # when /tmp/meetings exists, but nothing has changed. + result = self._run('subversion', [ "repo=\"http://svn.apache.org/repos/asf/subversion/trunk/notes/meetings\"","dest=\"/tmp/meetings\"" ]) + assert not result['changed'] + # when /tmp/meetings is a folder, but its not an svn repo + self._run('file',['path=/tmp/meetings','state=absent']) + self._run('file',['path=/tmp/meetings','state=directory']) + result = self._run('subversion', [ "repo=\"http://svn.apache.org/repos/asf/subversion/trunk/notes/meetings\"","dest=\"/tmp/meetings\"" ]) + assert result['failed'] + # when /tmp/meetings is a file + self._run('file',['path=/tmp/meetings','state=absent']) + self._run('command',['touch /tmp/meetings']) + result = self._run('subversion', [ "repo=\"http://svn.apache.org/repos/asf/subversion/trunk/notes/meetings\"","dest=\"/tmp/meetings\"" ]) + assert result['failed'] + # when /tmp/meetings is a folder, but its a different svn URL - should automatically switch + self._run('file',['path=/tmp/meetings','state=absent']) + result = self._run('subversion', [ "repo=\"http://svn.apache.org/repos/asf/subversion/trunk/notes/api-errata\"","dest=\"/tmp/meetings\"" ]) + assert result['changed'] + result = self._run('subversion', [ "repo=\"http://svn.apache.org/repos/asf/subversion/trunk/notes/meetings\"","dest=\"/tmp/meetings\"" ]) + assert result['changed'] + assert result['after'][1] == 'URL: http://svn.apache.org/repos/asf/subversion/trunk/notes/meetings' + # when /tmp/meetings is a folder, when its an older revision it should update + self._run('command',['svn up -r926415','chdir=/tmp/meetings']) + result = self._run('subversion', [ "repo=\"http://svn.apache.org/repos/asf/subversion/trunk/notes/meetings\"","dest=\"/tmp/meetings\"" ]) + assert result['changed'] + assert result['before'][0] == 'Revision: 926415' + assert result['after'][0] != 'Revision: 926415' + # when /tmp/meetings has dirty files in it, ignore them: + self._run('command',['touch /tmp/meetings/adirtyfile']) + result = self._run('subversion', [ "repo=\"http://svn.apache.org/repos/asf/subversion/trunk/notes/meetings\"","dest=\"/tmp/meetings\"" ]) + assert not result['changed'] # no changes to the repo + # when /tmp/meetings has modified file in it, fail: + self._run('file',['path=/tmp/meetings/adirtyfile','state=absent']) + self._run('command',['cp /tmp/meetings/berlin-11-agenda /tmp/meetings/svn-vision-agenda']) + result = self._run('subversion', [ "repo=\"http://svn.apache.org/repos/asf/subversion/trunk/notes/meetings\"","dest=\"/tmp/meetings\"","force=no" ]) + assert result['failed'] + # when /tmp/meetings has a modified file but force is set to yes, then just override it. + result = self._run('subversion', [ "repo=\"http://svn.apache.org/repos/asf/subversion/trunk/notes/meetings\"","dest=\"/tmp/meetings\"","force=yes" ]) + assert result['changed'] # no changes to the repo + def test_large_output(self): large_path = "/usr/share/dict/words" if not os.path.exists(large_path): @@ -198,5 +277,3 @@ class TestRunner(unittest.TestCase): "dest=%s" % output, ]) assert result['changed'] == False - - From de306922b12743154b699ec9b711737098f0011b Mon Sep 17 00:00:00 2001 From: Derek Carter Date: Thu, 23 Aug 2012 12:30:34 -0400 Subject: [PATCH 14/16] scope error with utils.py branch_path --- lib/ansible/utils.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/ansible/utils.py b/lib/ansible/utils.py index 5a0fb46b5c..3743337e74 100644 --- a/lib/ansible/utils.py +++ b/lib/ansible/utils.py @@ -317,17 +317,17 @@ def _gitinfo(): branch = f.readline().split('/')[-1].rstrip("\n") f.close() branch_path = os.path.join(repo_path, "refs", "heads", branch) - if os.path.exists(branch_path): - f = open(branch_path) - commit = f.readline()[:10] - f.close() - date = time.localtime(os.stat(branch_path).st_mtime) - if time.daylight == 0: - offset = time.timezone - else: - offset = time.altzone - result = "({0} {1}) last updated {2} (GMT {3:+04d})".format(branch, commit, - time.strftime("%Y/%m/%d %H:%M:%S", date), offset / -36) + if os.path.exists(branch_path): + f = open(branch_path) + commit = f.readline()[:10] + f.close() + date = time.localtime(os.stat(branch_path).st_mtime) + if time.daylight == 0: + offset = time.timezone + else: + offset = time.altzone + result = "({0} {1}) last updated {2} (GMT {3:+04d})".format(branch, commit, + time.strftime("%Y/%m/%d %H:%M:%S", date), offset / -36) else: result = 'n/a' return result From 8660fb074a82e816d8cecbd4e702e01fd8558f4c Mon Sep 17 00:00:00 2001 From: Jeroen Hoekx Date: Thu, 23 Aug 2012 19:41:26 +0200 Subject: [PATCH 15/16] Add the wait_for module. This module waits until a specific port on a given host can be connected to. --- library/wait_for | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 library/wait_for diff --git a/library/wait_for b/library/wait_for new file mode 100644 index 0000000000..201063d6a0 --- /dev/null +++ b/library/wait_for @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# (c) 2012, Jeroen Hoekx +# +# 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 . + +import socket +import datetime +import time +import sys + +def main(): + + module = AnsibleModule( + argument_spec = dict( + name=dict(required=True), + timeout=dict(default=300), + port=dict(default=22), + ), + ) + + params = module.params + + host = params['name'] + timeout = int(params['timeout']) + port = int(params['port']) + + end = datetime.datetime.now() + datetime.timedelta(seconds=timeout) + + while datetime.datetime.now() < end: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.connect( (host, port) ) + s.close() + break + except: + time.sleep(1) + else: + module.fail_json(msg="Timeout when waiting for %s"%(host)) + + module.exit_json(msg="%s responds on %s"%(host, port)) + +# this is magic, see lib/ansible/module_common.py +#<> +main() From b0a4a9e188f948aefd995fafc062a9cfb92119ad Mon Sep 17 00:00:00 2001 From: Ludovic Claude Date: Thu, 23 Aug 2012 20:26:27 +0200 Subject: [PATCH 16/16] Issue #935: filter out __init__ module from the list of callbacks --- lib/ansible/callbacks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ansible/callbacks.py b/lib/ansible/callbacks.py index 7b1c17c4ad..2011071d97 100644 --- a/lib/ansible/callbacks.py +++ b/lib/ansible/callbacks.py @@ -25,7 +25,7 @@ from ansible.color import stringc dirname = os.path.dirname(__file__) callbacks = utils.import_plugins(os.path.join(dirname, 'callback_plugins')) -callbacks = [ c.CallbackModule() for c in callbacks.values() ] +callbacks = [ c.CallbackModule() for c in callbacks.values() if c.__name__ != '__init__' ] cowsay = None if os.path.exists("/usr/bin/cowsay"):