From f4228d81d2103af443d05076acbf70f0ba6c7dac Mon Sep 17 00:00:00 2001 From: HAMSIK Adam Date: Thu, 6 Aug 2015 16:51:36 +0200 Subject: [PATCH 01/43] Convert enabled value to boolean to actually work, make sure we can set expiration period to 0(None) to disable it --- cloud/amazon/ec2_elb_lb.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/cloud/amazon/ec2_elb_lb.py b/cloud/amazon/ec2_elb_lb.py index 3d54f994436..856b6b3787a 100644 --- a/cloud/amazon/ec2_elb_lb.py +++ b/cloud/amazon/ec2_elb_lb.py @@ -755,21 +755,25 @@ class ElbManager(object): if self.stickiness['type'] == 'loadbalancer': policy = [] policy_type = 'LBCookieStickinessPolicyType' - if self.stickiness['enabled'] == True: + + if self.module.boolean(self.stickiness['enabled']) == True: if 'expiration' not in self.stickiness: self.module.fail_json(msg='expiration must be set when type is loadbalancer') + expiration = self.stickiness['expiration'] if self.stickiness['expiration'] is not 0 else None + policy_attrs = { 'type': policy_type, 'attr': 'lb_cookie_stickiness_policies', 'method': 'create_lb_cookie_stickiness_policy', 'dict_key': 'cookie_expiration_period', - 'param_value': self.stickiness['expiration'] + 'param_value': expiration } policy.append(self._policy_name(policy_attrs['type'])) + self._set_stickiness_policy(elb_info, listeners_dict, policy, **policy_attrs) - elif self.stickiness['enabled'] == False: + elif self.module.boolean(self.stickiness['enabled']) == False: if len(elb_info.policies.lb_cookie_stickiness_policies): if elb_info.policies.lb_cookie_stickiness_policies[0].policy_name == self._policy_name(policy_type): self.changed = True @@ -781,7 +785,7 @@ class ElbManager(object): elif self.stickiness['type'] == 'application': policy = [] policy_type = 'AppCookieStickinessPolicyType' - if self.stickiness['enabled'] == True: + if self.module.boolean(self.stickiness['enabled']) == True: if 'cookie' not in self.stickiness: self.module.fail_json(msg='cookie must be set when type is application') @@ -795,7 +799,7 @@ class ElbManager(object): } policy.append(self._policy_name(policy_attrs['type'])) self._set_stickiness_policy(elb_info, listeners_dict, policy, **policy_attrs) - elif self.stickiness['enabled'] == False: + elif self.module.boolean(self.stickiness['enabled']) == False: if len(elb_info.policies.app_cookie_stickiness_policies): if elb_info.policies.app_cookie_stickiness_policies[0].policy_name == self._policy_name(policy_type): self.changed = True From 8f03f1e4e142bf40ff84e7e163651bd83cad3885 Mon Sep 17 00:00:00 2001 From: Yann Hamon Date: Wed, 26 Aug 2015 00:01:35 +0200 Subject: [PATCH 02/43] Docker module: restarted should update the container when necessary --- cloud/docker/docker.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cloud/docker/docker.py b/cloud/docker/docker.py index 82c39006678..b542313079f 100644 --- a/cloud/docker/docker.py +++ b/cloud/docker/docker.py @@ -1586,6 +1586,10 @@ def restarted(manager, containers, count, name): containers.refresh() + for container in manager.get_differing_containers(): + manager.stop_containers([container]) + manager.remove_containers([container]) + manager.restart_containers(containers.running) started(manager, containers, count, name) From 08b09fcc7055307feb58b3ade88abf93babc94ff Mon Sep 17 00:00:00 2001 From: Jumpei Ogawa Date: Thu, 17 Sep 2015 15:31:14 +0900 Subject: [PATCH 03/43] Add better error message when specified network doesn't exist and ipv4_range is not specified --- cloud/google/gce_net.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/google/gce_net.py b/cloud/google/gce_net.py index 3ae1635ded7..269c05715dc 100644 --- a/cloud/google/gce_net.py +++ b/cloud/google/gce_net.py @@ -212,7 +212,7 @@ def main(): # user wants to create a new network that doesn't yet exist if name and not network: if not ipv4_range: - module.fail_json(msg="Missing required 'ipv4_range' parameter", + module.fail_json(msg="Network '" + name + "' is not found. To create network, 'ipv4_range' parameter is required", changed=False) try: From 7b4b61faffbff877167d33a11a0f6627912fc21e Mon Sep 17 00:00:00 2001 From: Jumpei Ogawa Date: Thu, 17 Sep 2015 15:37:07 +0900 Subject: [PATCH 04/43] Add explanation in case that ipv4_range is required --- cloud/google/gce_net.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/google/gce_net.py b/cloud/google/gce_net.py index 269c05715dc..5c412affb78 100644 --- a/cloud/google/gce_net.py +++ b/cloud/google/gce_net.py @@ -40,6 +40,7 @@ options: ipv4_range: description: - the IPv4 address range in CIDR notation for the network + this parameter is not mandatory when you specified existing network in name parameter, but when you create new network, this parameter is mandatory required: false aliases: ['cidr'] fwname: From 2080c8ab6e801ab62545624d2a47e92a802b48a0 Mon Sep 17 00:00:00 2001 From: Leonty Date: Fri, 25 Sep 2015 23:58:20 +0300 Subject: [PATCH 05/43] Support 'labels' parameter for docker. --- cloud/docker/docker.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/cloud/docker/docker.py b/cloud/docker/docker.py index 0ab564208ba..c22013bb933 100644 --- a/cloud/docker/docker.py +++ b/cloud/docker/docker.py @@ -320,6 +320,13 @@ options: default: false aliases: [] version_added: "2.0" + labels: + description: + - Set container labels. Requires docker >= 1.6 and docker-py >= 1.2.0. + requered: false + default: null + version_added: "1.9.4" + author: - "Cove Schneider (@cove)" - "Joshua Conner (@joshuaconner)" @@ -597,6 +604,7 @@ class DockerManager(object): 'cap_add': ((0, 5, 0), '1.14'), 'cap_drop': ((0, 5, 0), '1.14'), 'read_only': ((1, 0, 0), '1.17'), + 'labels': ((1, 2, 0), '1.18'), # Clientside only 'insecure_registry': ((0, 5, 0), '0.0') } @@ -1123,6 +1131,22 @@ class DockerManager(object): differing.append(container) continue + # LABELS + + expected_labels = {} + for name, value in self.module.params.get('labels').iteritems(): + expected_labels[name] = str(value) + + actual_labels = {} + for container_label in container['Config']['Labels'] or []: + name, value = container_label.split('=', 1) + actual_labels[name] = value + + if actual_labels != expected_labels: + self.reload_reasons.append('labels {0} => {1}'.format(actual_labels, expected_labels)) + differing.append(container) + continue + # HOSTNAME expected_hostname = self.module.params.get('hostname') @@ -1414,6 +1438,7 @@ class DockerManager(object): 'ports': self.exposed_ports, 'volumes': self.volumes, 'environment': self.env, + 'labels': self.module.params.get('labels'), 'hostname': self.module.params.get('hostname'), 'domainname': self.module.params.get('domainname'), 'detach': self.module.params.get('detach'), @@ -1668,6 +1693,7 @@ def main(): cap_add = dict(default=None, type='list'), cap_drop = dict(default=None, type='list'), read_only = dict(default=None, type='bool'), + labels = dict(default={}, type='dict'), ), required_together = ( ['tls_client_cert', 'tls_client_key'], From e96549c95d44120d885bcacfeacd3d6a56fce579 Mon Sep 17 00:00:00 2001 From: Andrew Pashkin Date: Fri, 2 Oct 2015 00:44:52 +0300 Subject: [PATCH 06/43] Harden matching running containers by "command" in the Docker module Before this patch: - Command was matched if 'Command' field of docker-py representation of Docker container ends with 'command' passed to Ansible docker module by user. - That can give false positives and false negatives. - For example: a) If 'command' was set up with more than one spaces, like 'command=sleep 123', it would be never matched again with a container(s) launched by this task. Because after launching, command would be normalized and appear, in docker-py API call, just as 'sleep 123' - with one space. This is false negative case. b) If 'entrypoint + command = command', for example 'sleep + 123 = sleep 123', module would give false positive match. This patch fixes it, by making matching more explicit - against 'Config'->Cmd' field of 'docker inspect' output, provided by docker-py API and with proper normalization of user input by splitting it to tokens with 'shlex.split()'. --- cloud/docker/docker.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/cloud/docker/docker.py b/cloud/docker/docker.py index 0ab564208ba..cefae3db3df 100644 --- a/cloud/docker/docker.py +++ b/cloud/docker/docker.py @@ -1314,8 +1314,8 @@ class DockerManager(object): """ command = self.module.params.get('command') - if command: - command = command.strip() + if command is not None: + command = shlex.split(command) name = self.module.params.get('name') if name and not name.startswith('/'): name = '/' + name @@ -1342,13 +1342,10 @@ class DockerManager(object): details = _docker_id_quirk(details) running_image = normalize_image(details['Config']['Image']) - running_command = container['Command'].strip() image_matches = running_image in repo_tags - # if a container has an entrypoint, `command` will actually equal - # '{} {}'.format(entrypoint, command) - command_matches = (not command or running_command.endswith(command)) + command_matches = command == details['Config']['Cmd'] matches = image_matches and command_matches From cee7e928fc2cb911480aae0c3ed53501034f4611 Mon Sep 17 00:00:00 2001 From: Andrew Pashkin Date: Fri, 2 Oct 2015 01:09:08 +0300 Subject: [PATCH 07/43] Add 'entrypoint' parameter to Docker module --- cloud/docker/docker.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/cloud/docker/docker.py b/cloud/docker/docker.py index cefae3db3df..3bc42629709 100644 --- a/cloud/docker/docker.py +++ b/cloud/docker/docker.py @@ -46,6 +46,14 @@ options: default: missing choices: [ "missing", "always" ] version_added: "1.9" + entrypoint: + description: + - Corresponds to ``--entrypoint`` option of ``docker run`` command and + ``ENTRYPOINT`` directive of Dockerfile. + Used to match and launch containers. + default: null + required: false + version_added: "2.0" command: description: - Command used to match and launch containers. @@ -1043,6 +1051,21 @@ class DockerManager(object): differing.append(container) continue + # ENTRYPOINT + + expected_entrypoint = self.module.params.get('entrypoint') + if expected_entrypoint: + expected_entrypoint = shlex.split(expected_entrypoint) + actual_entrypoint = container["Config"]["Entrypoint"] + + if actual_entrypoint != expected_entrypoint: + self.reload_reasons.append( + 'entrypoint ({0} => {1})' + .format(actual_entrypoint, expected_entrypoint) + ) + differing.append(container) + continue + # COMMAND expected_command = self.module.params.get('command') @@ -1313,6 +1336,9 @@ class DockerManager(object): Return any matching containers that are already present. """ + entrypoint = self.module.params.get('entrypoint') + if entrypoint is not None: + entrypoint = shlex.split(entrypoint) command = self.module.params.get('command') if command is not None: command = shlex.split(command) @@ -1346,8 +1372,12 @@ class DockerManager(object): image_matches = running_image in repo_tags command_matches = command == details['Config']['Cmd'] + entrypoint_matches = ( + entrypoint == details['Config']['Entrypoint'] + ) - matches = image_matches and command_matches + matches = (image_matches and command_matches and + entrypoint_matches) if matches: if not details: @@ -1407,6 +1437,7 @@ class DockerManager(object): api_version = self.client.version()['ApiVersion'] params = {'image': self.module.params.get('image'), + 'entrypoint': self.module.params.get('entrypoint'), 'command': self.module.params.get('command'), 'ports': self.exposed_ports, 'volumes': self.volumes, @@ -1619,6 +1650,7 @@ def main(): count = dict(default=1), image = dict(required=True), pull = dict(required=False, default='missing', choices=['missing', 'always']), + entrypoint = dict(required=False, default=None, type='str'), command = dict(required=False, default=None), expose = dict(required=False, default=None, type='list'), ports = dict(required=False, default=None, type='list'), From 81e9d1bde56f89edb2ca370a3c9e6838cff50072 Mon Sep 17 00:00:00 2001 From: Aaron Boushley Date: Fri, 9 Oct 2015 16:11:44 -0700 Subject: [PATCH 08/43] Fix issue with comparing versions improperly. This allows old versions of docker api to function. --- cloud/docker/docker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/docker/docker.py b/cloud/docker/docker.py index 0ab564208ba..ab71eb25b66 100644 --- a/cloud/docker/docker.py +++ b/cloud/docker/docker.py @@ -1088,7 +1088,7 @@ class DockerManager(object): self.module.fail_json(msg=str(e)) #For v1.19 API and above use HostConfig, otherwise use Config - if api_version >= 1.19: + if docker.utils.compare_version('1.19', api_version) >= 0: actual_mem = container['HostConfig']['Memory'] else: actual_mem = container['Config']['Memory'] @@ -1427,7 +1427,7 @@ class DockerManager(object): params['host_config'] = self.create_host_config() #For v1.19 API and above use HostConfig, otherwise use Config - if api_version < 1.19: + if docker.utils.compare_version('1.19', api_version) < 0: params['mem_limit'] = mem_limit else: params['host_config']['Memory'] = mem_limit From acdde856c5908f61dc4f6d75e8a9d42bc51c3ee8 Mon Sep 17 00:00:00 2001 From: Lee Hardy Date: Thu, 22 Oct 2015 13:45:50 +0100 Subject: [PATCH 09/43] - mysql: add user_anonymous parameter, which interacts with anonymous users - mysql; add host_all parameter, which forces iteration over all 'user'@... matches --- database/mysql/mysql_user.py | 166 ++++++++++++++++++++++++----------- 1 file changed, 113 insertions(+), 53 deletions(-) diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index 1ea54b41b3a..acf093f8490 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -30,6 +30,13 @@ options: description: - name of the user (role) to add or remove required: true + user_anonymous: + description: + - username is to be ignored and anonymous users with no username + handled + required: false + choices: [ "yes", "no" ] + default: no password: description: - set the user's password @@ -40,6 +47,14 @@ options: - the 'host' part of the MySQL username required: false default: localhost + host_all: + description: + - override the host option, making ansible apply changes to + all hostnames for a given user. This option cannot be used + when creating users + required: false + choices: [ "yes", "no" ] + default: "no" login_user: description: - The username used to authenticate with @@ -133,9 +148,12 @@ EXAMPLES = """ # Modify user Bob to require SSL connections. Note that REQUIRESSL is a special privilege that should only apply to *.* by itself. - mysql_user: name=bob append_privs=true priv=*.*:REQUIRESSL state=present -# Ensure no user named 'sally' exists, also passing in the auth credentials. +# Ensure no user named 'sally'@'localhost' exists, also passing in the auth credentials. - mysql_user: login_user=root login_password=123456 name=sally state=absent +# Ensure no user named 'sally' exists at all +- mysql_user: name=sally host_all=yes state=absent + # Specify grants composed of more than one word - mysql_user: name=replication password=12345 priv=*.*:"REPLICATION CLIENT" state=present @@ -206,71 +224,104 @@ def connect(module, login_user=None, login_password=None, config_file=''): db_connection = MySQLdb.connect(**config) return db_connection.cursor() -def user_exists(cursor, user, host): - cursor.execute("SELECT count(*) FROM user WHERE user = %s AND host = %s", (user,host)) +def user_exists(cursor, user, host, host_all): + if host_all: + cursor.execute("SELECT count(*) FROM user WHERE user = %s AND host = %s", (user,host)) + else: + cursor.execute("SELECT count(*) FROM user WHERE user = %s AND host = %s", (user,host)) + count = cursor.fetchone() return count[0] > 0 -def user_add(cursor, user, host, password, new_priv): +def user_add(cursor, user, host, host_all, password, new_priv): + # we cannot create users without a proper hostname + if host_all: + return False + 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): +def user_mod(cursor, user, host, host_all, password, new_priv, append_privs): changed = False grant_option = False - # 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,priv,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) + # to simplify code, if we have a specific host and no host_all, we create + # a list with just host and loop over that + if host_all: + hostnames = user_get_hostnames(cursor, user) + else: + hostnames = [host] + + for host in hostnames: + # 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 - # 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): - if not append_privs: - privileges_revoke(cursor, user,host,db_table,curr_priv[db_table],grant_option) - privileges_grant(cursor, user,host,db_table,new_priv[db_table]) - 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,priv,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): + if not append_privs: + privileges_revoke(cursor, user,host,db_table,curr_priv[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)) +def user_delete(cursor, user, host, host_all): + if host_all: + hostnames = user_get_hostnames(cursor, user) + + for hostname in hostnames: + cursor.execute("DROP USER %s@%s", (user, hostname)) + else: + cursor.execute("DROP USER %s@%s", (user, host)) + return True +def user_get_hostnames(cursor, user): + cursor.execute("SELECT Host FROM mysql.user WHERE user = %s", user) + hostnames_raw = cursor.fetchall() + hostnames = [] + + for hostname_raw in hostnames_raw: + hostnames.append(hostname_raw[0]) + + return hostnames + 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. @@ -387,8 +438,10 @@ def main(): login_port=dict(default=3306, type='int'), login_unix_socket=dict(default=None), user=dict(required=True, aliases=['name']), + user_anonymous=dict(type="bool", default="no"), password=dict(default=None, no_log=True), host=dict(default="localhost"), + host_all=dict(type="bool", default="no"), state=dict(default="present", choices=["absent", "present"]), priv=dict(default=None), append_privs=dict(default=False, type='bool'), @@ -400,8 +453,10 @@ def main(): login_user = module.params["login_user"] login_password = module.params["login_password"] user = module.params["user"] + user_anonymous = module.params["user_anonymous"] password = module.params["password"] host = module.params["host"].lower() + host_all = module.params["host_all"] state = module.params["state"] priv = module.params["priv"] check_implicit_admin = module.params['check_implicit_admin'] @@ -409,6 +464,9 @@ def main(): append_privs = module.boolean(module.params["append_privs"]) update_password = module.params['update_password'] + if user_anonymous: + user = '' + config_file = os.path.expanduser(os.path.expandvars(config_file)) if not mysqldb_found: module.fail_json(msg="the python mysqldb module is required") @@ -433,25 +491,27 @@ def main(): module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or ~/.my.cnf has the credentials. Exception message: %s" % e) if state == "present": - if user_exists(cursor, user, host): + if user_exists(cursor, user, host, host_all): try: if update_password == 'always': - changed = user_mod(cursor, user, host, password, priv, append_privs) + changed = user_mod(cursor, user, host, host_all, password, priv, append_privs) else: - changed = user_mod(cursor, user, host, None, priv, append_privs) + changed = user_mod(cursor, user, host, host_all, None, priv, append_privs) except (SQLParseError, InvalidPrivsError, MySQLdb.Error), e: module.fail_json(msg=str(e)) else: if password is None: module.fail_json(msg="password parameter required when adding a user") + if host_all: + module.fail_json(msg="host_all parameter cannot be used when adding a user") try: - changed = user_add(cursor, user, host, password, priv) + changed = user_add(cursor, user, host, host_all, password, priv) except (SQLParseError, InvalidPrivsError, MySQLdb.Error), e: module.fail_json(msg=str(e)) elif state == "absent": - if user_exists(cursor, user, host): - changed = user_delete(cursor, user, host) + if user_exists(cursor, user, host, host_all): + changed = user_delete(cursor, user, host, host_all) else: changed = False module.exit_json(changed=changed, user=user) From a95fee40793666b534898bacb2fb145b361d86d9 Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Thu, 6 Aug 2015 13:34:25 +0100 Subject: [PATCH 10/43] Use 'pip freeze' output to detect changes with requirement specified If the requirements contains a repos url it will always report 'Successfully installed'; there is no difference in the output to tell apart if anything new was pulled. Use freeze to detect if the environment changed in any way. Should fix ansible/ansible#1705 --- packaging/language/pip.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packaging/language/pip.py b/packaging/language/pip.py index bdd2b40a1aa..3b5f396ab45 100755 --- a/packaging/language/pip.py +++ b/packaging/language/pip.py @@ -363,6 +363,12 @@ def main(): changed = (state == 'present' and not is_present) or (state == 'absent' and is_present) module.exit_json(changed=changed, cmd=freeze_cmd, stdout=out, stderr=err) + if requirements: + freeze_cmd = '%s freeze' % pip + out_freeze_before = module.run_command(freeze_cmd, cwd=chdir)[1] + else: + out_freeze_before = None + rc, out_pip, err_pip = module.run_command(cmd, path_prefix=path_prefix, cwd=chdir) out += out_pip err += err_pip @@ -375,7 +381,11 @@ def main(): if state == 'absent': changed = 'Successfully uninstalled' in out_pip else: - changed = 'Successfully installed' in out_pip + if out_freeze_before is None: + changed = 'Successfully installed' in out_pip + else: + out_freeze_after = module.run_command(freeze_cmd, cwd=chdir)[1] + changed = out_freeze_before != out_freeze_after module.exit_json(changed=changed, cmd=cmd, name=name, version=version, state=state, requirements=requirements, virtualenv=env, From c860af29b28a1c66a13d8e9d4d8d7a518ae9a75c Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Thu, 6 Aug 2015 14:24:41 +0100 Subject: [PATCH 11/43] Detect unchanged pip runs when using a vcs url in name Should fix bug #1645 --- packaging/language/pip.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packaging/language/pip.py b/packaging/language/pip.py index 3b5f396ab45..6d325282770 100755 --- a/packaging/language/pip.py +++ b/packaging/language/pip.py @@ -20,6 +20,7 @@ # import tempfile +import re import os DOCUMENTATION = ''' @@ -321,17 +322,15 @@ def main(): # Automatically apply -e option to extra_args when source is a VCS url. VCS # includes those beginning with svn+, git+, hg+ or bzr+ - if name: - if module.params['editable']: - if name.startswith('svn+') or name.startswith('git+') or \ - name.startswith('hg+') or name.startswith('bzr+'): - args_list = [] # used if extra_args is not used at all - if extra_args: - args_list = extra_args.split(' ') - if '-e' not in args_list: - args_list.append('-e') - # Ok, we will reconstruct the option string - extra_args = ' '.join(args_list) + has_vcs = bool(name and re.match(r'(svn|git|hg|bzr)\+', name)) + if has_vcs and module.params['editable']: + args_list = [] # used if extra_args is not used at all + if extra_args: + args_list = extra_args.split(' ') + if '-e' not in args_list: + args_list.append('-e') + # Ok, we will reconstruct the option string + extra_args = ' '.join(args_list) if extra_args: cmd += ' %s' % extra_args @@ -344,8 +343,7 @@ def main(): if module.check_mode: if extra_args or requirements or state == 'latest' or not name: module.exit_json(changed=True) - elif name.startswith('svn+') or name.startswith('git+') or \ - name.startswith('hg+') or name.startswith('bzr+'): + elif has_vcs: module.exit_json(changed=True) freeze_cmd = '%s freeze' % pip @@ -363,7 +361,7 @@ def main(): changed = (state == 'present' and not is_present) or (state == 'absent' and is_present) module.exit_json(changed=changed, cmd=freeze_cmd, stdout=out, stderr=err) - if requirements: + if requirements or has_vcs: freeze_cmd = '%s freeze' % pip out_freeze_before = module.run_command(freeze_cmd, cwd=chdir)[1] else: From 5b904c1401dee1f6c02780aeb696c01bea398762 Mon Sep 17 00:00:00 2001 From: Leonty Date: Wed, 4 Nov 2015 12:43:03 +0300 Subject: [PATCH 12/43] Corrected misspelling in the 'labels' docker parameter documentation. --- cloud/docker/docker.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cloud/docker/docker.py b/cloud/docker/docker.py index c22013bb933..c94df54ad89 100644 --- a/cloud/docker/docker.py +++ b/cloud/docker/docker.py @@ -323,10 +323,9 @@ options: labels: description: - Set container labels. Requires docker >= 1.6 and docker-py >= 1.2.0. - requered: false + required: false default: null version_added: "1.9.4" - author: - "Cove Schneider (@cove)" - "Joshua Conner (@joshuaconner)" From 2aeb188d81a22d030398ff4018b5cf676ca0e5f4 Mon Sep 17 00:00:00 2001 From: Lee Hardy Date: Wed, 4 Nov 2015 16:37:18 +0000 Subject: [PATCH 13/43] - fix user_exists statement with host_all to use only username parameter --- database/mysql/mysql_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index acf093f8490..d63fd41f44f 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -226,7 +226,7 @@ def connect(module, login_user=None, login_password=None, config_file=''): def user_exists(cursor, user, host, host_all): if host_all: - cursor.execute("SELECT count(*) FROM user WHERE user = %s AND host = %s", (user,host)) + cursor.execute("SELECT count(*) FROM user WHERE user = %s", user) else: cursor.execute("SELECT count(*) FROM user WHERE user = %s AND host = %s", (user,host)) From 9618986804428c29670f2409903d84e9e3df6950 Mon Sep 17 00:00:00 2001 From: Andy Nelson Date: Tue, 11 Aug 2015 19:51:59 +0100 Subject: [PATCH 14/43] Update to ec2_vpc.py to: 1 allow interface ids and vpc peering connections as route targets 2 set state to "terminated" when VPC is removed 3 fix some comment typos updates per PR comments --- cloud/amazon/ec2_vpc.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/cloud/amazon/ec2_vpc.py b/cloud/amazon/ec2_vpc.py index a3003a6dcc6..741e73de479 100644 --- a/cloud/amazon/ec2_vpc.py +++ b/cloud/amazon/ec2_vpc.py @@ -72,7 +72,7 @@ options: aliases: [] route_tables: description: - - 'A dictionary array of route tables to add of the form: { subnets: [172.22.2.0/24, 172.22.3.0/24,], routes: [{ dest: 0.0.0.0/0, gw: igw},], resource_tags: ... }. Where the subnets list is those subnets the route table should be associated with, and the routes list is a list of routes to be in the table. The special keyword for the gw of igw specifies that you should the route should go through the internet gateway attached to the VPC. gw also accepts instance-ids in addition igw. resource_tags is optional and uses dictionary form: { "Name": "public", ... }. This module is currently unable to affect the "main" route table due to some limitations in boto, so you must explicitly define the associated subnets or they will be attached to the main table implicitly. As of 1.8, if the route_tables parameter is not specified, no existing routes will be modified.' + - 'A dictionary array of route tables to add of the form: { subnets: [172.22.2.0/24, 172.22.3.0/24,], routes: [{ dest: 0.0.0.0/0, gw: igw},], resource_tags: ... }. Where the subnets list is those subnets the route table should be associated with, and the routes list is a list of routes to be in the table. The special keyword for the gw of igw specifies that you should the route should go through the internet gateway attached to the VPC. gw also accepts instance-ids, interface-ids, and vpc-peering-connection-ids in addition igw. resource_tags is optional and uses dictionary form: { "Name": "public", ... }. This module is currently unable to affect the "main" route table due to some limitations in boto, so you must explicitly define the associated subnets or they will be attached to the main table implicitly. As of 1.8, if the route_tables parameter is not specified, no existing routes will be modified.' required: false default: null aliases: [] @@ -234,25 +234,29 @@ def routes_match(rt_list=None, rt=None, igw=None): Returns: True when there provided routes and remote routes are the same. - False when provided routes and remote routes are diffrent. + False when provided routes and remote routes are different. """ local_routes = [] remote_routes = [] for route in rt_list: - route_kwargs = {} + route_kwargs = { + 'gateway_id': None, + 'instance_id': None, + 'interface_id': None, + 'vpc_peering_connection_id': None, + 'state': 'active' + } if route['gw'] == 'igw': route_kwargs['gateway_id'] = igw.id - route_kwargs['instance_id'] = None - route_kwargs['state'] = 'active' elif route['gw'].startswith('i-'): route_kwargs['instance_id'] = route['gw'] - route_kwargs['gateway_id'] = None - route_kwargs['state'] = 'active' + elif route['gw'].startswith('eni-'): + route_kwargs['interface_id'] = route['gw'] + elif route['gw'].startswith('pcx-'): + route_kwargs['vpc_peering_connection_id'] = route['gw'] else: route_kwargs['gateway_id'] = route['gw'] - route_kwargs['instance_id'] = None - route_kwargs['state'] = 'active' route_kwargs['destination_cidr_block'] = route['dest'] local_routes.append(route_kwargs) for j in rt.routes: @@ -280,7 +284,7 @@ def rtb_changed(route_tables=None, vpc_conn=None, module=None, vpc=None, igw=Non igw : The internet gateway object for this vpc Returns: - True when there is diffrence beween the provided routes and remote routes and if subnet assosications are diffrent. + True when there is difference between the provided routes and remote routes and if subnet associations are different. False when both routes and subnet associations matched. """ @@ -509,6 +513,10 @@ def create_vpc(module, vpc_conn): route_kwargs['gateway_id'] = igw.id elif route['gw'].startswith('i-'): route_kwargs['instance_id'] = route['gw'] + elif route['gw'].startswith('eni-'): + route_kwargs['interface_id'] = route['gw'] + elif route['gw'].startswith('pcx-'): + route_kwargs['vpc_peering_connection_id'] = route['gw'] else: route_kwargs['gateway_id'] = route['gw'] vpc_conn.create_route(new_rt.id, route['dest'], **route_kwargs) @@ -652,6 +660,7 @@ def terminate_vpc(module, vpc_conn, vpc_id=None, cidr=None): msg='Unable to delete VPC {0}, error: {1}'.format(vpc.id, e) ) changed = True + vpc_dict['state'] = "terminated" return (changed, vpc_dict, terminated_vpc_id) From 794911c592345c8343a38f7f90cc9ed2d727fb2f Mon Sep 17 00:00:00 2001 From: luto Date: Thu, 26 Nov 2015 13:32:27 +0100 Subject: [PATCH 15/43] vsphere_guest: support putting a guest into a nested folder --- cloud/vmware/vsphere_guest.py | 62 ++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/cloud/vmware/vsphere_guest.py b/cloud/vmware/vsphere_guest.py index a14f807e049..fdfbac5876d 100644 --- a/cloud/vmware/vsphere_guest.py +++ b/cloud/vmware/vsphere_guest.py @@ -175,6 +175,10 @@ EXAMPLES = ''' size_gb: 10 type: thin datastore: storage001 + # VMs can be put into folders. The value given here is either the full path + # to the folder (e.g. production/customerA/lamp) or just the last component + # of the path (e.g. lamp): + folder: production/customerA/lamp vm_nic: nic1: type: vmxnet3 @@ -915,6 +919,48 @@ def reconfigure_net(vsphere_client, vm, module, esxi, resource_pool, guest, vm_n elif len(nics) == 0: return(False) + +def _build_folder_tree(nodes, parent): + tree = {} + + for node in nodes: + if node['parent'] == parent: + tree[node['name']] = dict.copy(node) + tree[node['name']]['subfolders'] = _build_folder_tree(nodes, node['id']) + del tree[node['name']]['parent'] + + return tree + + +def _find_path_in_tree(tree, path): + for name, o in tree.iteritems(): + if name == path[0]: + if len(path) == 1: + return o + else: + return _find_path_in_tree(o['subfolders'], path[1:]) + + return None + + +def _get_folderid_for_path(vsphere_client, datacenter, path): + content = vsphere_client._retrieve_properties_traversal(property_names=['name', 'parent'], obj_type=MORTypes.Folder) + if not content: return {} + + node_list = [ + { + 'id': o.Obj, + 'name': o.PropSet[0].Val, + 'parent': (o.PropSet[1].Val if len(o.PropSet) > 1 else None) + } for o in content + ] + + tree = _build_folder_tree(node_list, datacenter) + tree = _find_path_in_tree(tree, ['vm'])['subfolders'] + folder = _find_path_in_tree(tree, path.split('/')) + return folder['id'] if folder else None + + def create_vm(vsphere_client, module, esxi, resource_pool, cluster_name, guest, vm_extra_config, vm_hardware, vm_disk, vm_nic, vm_hw_version, state): datacenter = esxi['datacenter'] @@ -935,13 +981,19 @@ def create_vm(vsphere_client, module, esxi, resource_pool, cluster_name, guest, # virtualmachineFolder managed object reference if vm_extra_config.get('folder'): - if vm_extra_config['folder'] not in vsphere_client._get_managed_objects(MORTypes.Folder).values(): + # try to find the folder by its full path, e.g. 'production/customerA/lamp' + vmfmor = _get_folderid_for_path(vsphere_client, dcmor, vm_extra_config.get('folder')) + + # try the legacy behaviour of just matching the folder name, so 'lamp' alone matches 'production/customerA/lamp' + if vmfmor is None: + for mor, name in vsphere_client._get_managed_objects(MORTypes.Folder).iteritems(): + if name == vm_extra_config['folder']: + vmfmor = mor + + # if neither of strategies worked, bail out + if vmfmor is None: vsphere_client.disconnect() module.fail_json(msg="Cannot find folder named: %s" % vm_extra_config['folder']) - - for mor, name in vsphere_client._get_managed_objects(MORTypes.Folder).iteritems(): - if name == vm_extra_config['folder']: - vmfmor = mor else: vmfmor = dcprops.vmFolder._obj From 18f4f5dcc6121d85c57f8448aeee6f26710d4a2d Mon Sep 17 00:00:00 2001 From: Michel Alexandre Salim Date: Wed, 9 Dec 2015 10:07:16 +0700 Subject: [PATCH 16/43] Set the argument type for ec2_vol's encrypted parameter If this is not set, Ansible parses the parameter as a string. This is fine if the parameter is not provided by the caller, but if it is set to False or True explicitly, ec2_vol receives this as the string 'False' or the string 'True', both of which are truthy. Thus, without this fix, setting the parameter results in encryption always enabled. --- cloud/amazon/ec2_vol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vol.py b/cloud/amazon/ec2_vol.py index 62e36a74ced..4f1dbf33114 100644 --- a/cloud/amazon/ec2_vol.py +++ b/cloud/amazon/ec2_vol.py @@ -379,7 +379,7 @@ def main(): volume_size = dict(), volume_type = dict(choices=['standard', 'gp2', 'io1'], default='standard'), iops = dict(), - encrypted = dict(), + encrypted = dict(type='bool', default=False), device_name = dict(), zone = dict(aliases=['availability_zone', 'aws_zone', 'ec2_zone']), snapshot = dict(), From 0125770d8deaaf5770ae37890a0e051ee000d8f3 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Sun, 13 Dec 2015 09:16:28 -0800 Subject: [PATCH 17/43] Use rpm instead of repoquery for is_installed() * This keeps us from hitting bugs in repoquery/yum plugins in certain instances (#2559). * The previous is also a small performance boost * Also in is_installed(), when using the yum API, return if we detect a package name has been installed. We don't need to also check virtual provides in that case. This is another small performance boost. * Sort the list of packages returned by the list parameter. --- packaging/os/yum.py | 55 ++++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/packaging/os/yum.py b/packaging/os/yum.py index 783794690f0..f9b5c41ef02 100644 --- a/packaging/os/yum.py +++ b/packaging/os/yum.py @@ -21,8 +21,6 @@ # along with Ansible. If not, see . # - -import traceback import os import yum import rpm @@ -189,6 +187,7 @@ EXAMPLES = ''' BUFSIZE = 65536 def_qf = "%{name}-%{version}-%{release}.%{arch}" +rpmbin = None def yum_base(conf_file=None): @@ -232,8 +231,8 @@ def is_installed(module, repoq, pkgspec, conf_file, qf=def_qf, en_repos=None, di en_repos = [] if dis_repos is None: dis_repos = [] - if not repoq: + if not repoq: pkgs = [] try: my = yum_base(conf_file) @@ -241,10 +240,10 @@ def is_installed(module, repoq, pkgspec, conf_file, qf=def_qf, en_repos=None, di my.repos.disableRepo(rid) for rid in en_repos: my.repos.enableRepo(rid) - + e, m, u = my.rpmdb.matchPackageNames([pkgspec]) pkgs = e + m - if not pkgs: + if not pkgs and not is_pkg: pkgs.extend(my.returnInstalledPackagesByDep(pkgspec)) except Exception, e: module.fail_json(msg="Failure talking to yum: %s" % e) @@ -252,21 +251,31 @@ def is_installed(module, repoq, pkgspec, conf_file, qf=def_qf, en_repos=None, di return [ po_to_nevra(p) for p in pkgs ] else: + global rpmbin + if not rpmbin: + rpmbin = module.get_bin_path('rpm', required=True) - cmd = repoq + ["--disablerepo=*", "--pkgnarrow=installed", "--qf", qf, pkgspec] + cmd = [rpmbin, '-q', '--qf', qf, pkgspec] rc, out, err = module.run_command(cmd) - if not is_pkg: - cmd = repoq + ["--disablerepo=*", "--pkgnarrow=installed", "--qf", qf, "--whatprovides", pkgspec] + if rc != 0 and 'is not installed' not in out: + module.fail_json(msg='Error from rpm: %s: %s' % (cmd, err)) + if 'is not installed' in out: + out = '' + + pkgs = [p for p in out.replace('(none)', '0').split('\n') if p.strip()] + if not pkgs and not is_pkg: + cmd = [rpmbin, '-q', '--qf', qf, '--whatprovides', pkgspec] rc2, out2, err2 = module.run_command(cmd) else: rc2, out2, err2 = (0, '', '') - - if rc == 0 and rc2 == 0: - out += out2 - return [p for p in out.split('\n') if p.strip()] - else: - module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err + err2)) - + + if rc2 != 0 and 'no package provides' not in out2: + module.fail_json(msg='Error from rpm: %s: %s' % (cmd, err + err2)) + if 'no package provides' in out2: + out2 = '' + pkgs += [p for p in out2.replace('(none)', '0').split('\n') if p.strip()] + return pkgs + return [] def is_available(module, repoq, pkgspec, conf_file, qf=def_qf, en_repos=None, dis_repos=None): @@ -506,20 +515,22 @@ def repolist(module, repoq, qf="%{repoid}"): def list_stuff(module, repoquerybin, conf_file, stuff): qf = "%{name}|%{epoch}|%{version}|%{release}|%{arch}|%{repoid}" + # is_installed goes through rpm instead of repoquery so it needs a slightly different format + is_installed_qf = "%{name}|%{epoch}|%{version}|%{release}|%{arch}|installed\n" repoq = [repoquerybin, '--show-duplicates', '--plugins', '--quiet'] if conf_file and os.path.exists(conf_file): repoq += ['-c', conf_file] if stuff == 'installed': - return [ pkg_to_dict(p) for p in is_installed(module, repoq, '-a', conf_file, qf=qf) if p.strip() ] + return [ pkg_to_dict(p) for p in sorted(is_installed(module, repoq, '-a', conf_file, qf=is_installed_qf)) if p.strip() ] elif stuff == 'updates': - return [ pkg_to_dict(p) for p in is_update(module, repoq, '-a', conf_file, qf=qf) if p.strip() ] + return [ pkg_to_dict(p) for p in sorted(is_update(module, repoq, '-a', conf_file, qf=qf)) if p.strip() ] elif stuff == 'available': - return [ pkg_to_dict(p) for p in is_available(module, repoq, '-a', conf_file, qf=qf) if p.strip() ] + return [ pkg_to_dict(p) for p in sorted(is_available(module, repoq, '-a', conf_file, qf=qf)) if p.strip() ] elif stuff == 'repos': - return [ dict(repoid=name, state='enabled') for name in repolist(module, repoq) if name.strip() ] + return [ dict(repoid=name, state='enabled') for name in sorted(repolist(module, repoq)) if name.strip() ] else: - return [ pkg_to_dict(p) for p in is_installed(module, repoq, stuff, conf_file, qf=qf) + is_available(module, repoq, stuff, conf_file, qf=qf) if p.strip() ] + return [ pkg_to_dict(p) for p in sorted(is_installed(module, repoq, stuff, conf_file, qf=is_installed_qf) + is_available(module, repoq, stuff, conf_file, qf=qf)) if p.strip() ] def install(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos): @@ -951,6 +962,7 @@ def ensure(module, state, pkgs, conf_file, enablerepo, disablerepo, return res + def main(): # state=installed name=pkgspec @@ -1022,7 +1034,8 @@ def main(): results = ensure(module, state, pkg, params['conf_file'], enablerepo, disablerepo, disable_gpg_check, exclude, repoquery) if repoquery: - results['msg'] = '%s %s' % (results.get('msg',''), 'Warning: Due to potential bad behaviour with rhnplugin and certificates, used slower repoquery calls instead of Yum API.') + results['msg'] = '%s %s' % (results.get('msg',''), + 'Warning: Due to potential bad behaviour with rhnplugin and certificates, used slower repoquery calls instead of Yum API.') module.exit_json(**results) From 9dd6cad22460d7595cc6caf8f28da8e09fbd9a91 Mon Sep 17 00:00:00 2001 From: Lee H Date: Mon, 14 Dec 2015 11:46:32 -0500 Subject: [PATCH 18/43] - add example showing removal of anonymous user accounts --- database/mysql/mysql_user.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index 84b52a95d3f..aa7f19a4415 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -147,6 +147,12 @@ author: "Jonathan Mainguy (@Jmainguy)" ''' EXAMPLES = """ +# Removes anonymous user account for localhost (the name parameter is required, but ignored) +- mysql_user: name=anonymous user_anonymous=yes host=localhost state=absent + +# Removes all anonymous user accounts +- mysql_user: name=anonymous user_anonymous=yes host_all=yes state=absent + # Create database user with name 'bob' and password '12345' with all database privileges - mysql_user: name=bob password=12345 priv=*.*:ALL state=present From f500a2ec53a2897996b8175744249af8fa37a360 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 14 Dec 2015 21:18:13 -0500 Subject: [PATCH 19/43] added mime option to stat module it uses file magic to now return mime_type and charset of a file as per output of `file -i /path` --- files/stat.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/files/stat.py b/files/stat.py index 1e41185ad6a..02c78f46d46 100644 --- a/files/stat.py +++ b/files/stat.py @@ -55,6 +55,15 @@ options: default: sha1 aliases: [ 'checksum_algo' ] version_added: "2.0" + mime: + description: + - Use file magic and return data about the nature of the file. this uses the 'file' utility found on most Linux/Unix systems. + - This will add both `mime_type` and 'charset' fields to the return, if possible. + required: false + choices: [ Yes, No ] + default: No + version_added: "2.1" + aliases: [ 'mime_type', 'mime-type' ] author: "Bruce Pennypacker (@bpennypacker)" ''' @@ -278,6 +287,16 @@ stat: returned: success, path exists and user can read stats and installed python supports it type: string sample: www-data + mime_type: + description: file magic data or mime-type + returned: success, path exists and user can read stats and installed python supports it and the `mime` option was true, will return 'unknown' on error. + type: string + sample: PDF document, version 1.2 + charset: + description: file character set or encoding + returned: success, path exists and user can read stats and installed python supports it and the `mime` option was true, will return 'unknown' on error. + type: string + sample: us-ascii ''' import os @@ -293,7 +312,8 @@ def main(): follow = dict(default='no', type='bool'), get_md5 = dict(default='yes', type='bool'), get_checksum = dict(default='yes', type='bool'), - checksum_algorithm = dict(default='sha1', type='str', choices=['sha1', 'sha224', 'sha256', 'sha384', 'sha512'], aliases=['checksum_algo']) + checksum_algorithm = dict(default='sha1', type='str', choices=['sha1', 'sha224', 'sha256', 'sha384', 'sha512'], aliases=['checksum_algo']), + mime = dict(default=False, type='bool', aliases=['mime_type', 'mime-type']), ), supports_check_mode = True ) @@ -376,6 +396,19 @@ def main(): except: pass + if module.params.get('mime'): + d['mime_type'] = 'unknown' + d['charset'] = 'unknown' + + filecmd = [module.get_bin_path('file', True),'-i', path] + try: + rc, out, err = module.run_command(filecmd) + if rc == 0: + mtype, chset = out.split(':')[1].split(';') + d['mime_type'] = mtype.strip() + d['charset'] = chset.split('=')[1].strip() + except: + pass module.exit_json(changed=False, stat=d) From 0cacadb670b6359446dc7fba1130c4d800fd0664 Mon Sep 17 00:00:00 2001 From: Tobias Smolka Date: Tue, 15 Dec 2015 12:45:51 +0100 Subject: [PATCH 20/43] Making cluster parameter optional --- cloud/vmware/vsphere_guest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/vmware/vsphere_guest.py b/cloud/vmware/vsphere_guest.py index f5507b331c2..3b03ec3da78 100644 --- a/cloud/vmware/vsphere_guest.py +++ b/cloud/vmware/vsphere_guest.py @@ -656,7 +656,7 @@ def deploy_template(vsphere_client, guest, resource_pool, template_src, esxi, mo elif resource_pool: try: cluster = [k for k, - v in vsphere_client.get_clusters().items() if v == cluster_name][0] + v in vsphere_client.get_clusters().items() if v == cluster_name][0] if cluster_name else None except IndexError, e: vsphere_client.disconnect() module.fail_json(msg="Cannot find Cluster named: %s" % @@ -1059,7 +1059,7 @@ def create_vm(vsphere_client, module, esxi, resource_pool, cluster_name, guest, if resource_pool: try: cluster = [k for k, - v in vsphere_client.get_clusters().items() if v == cluster_name][0] + v in vsphere_client.get_clusters().items() if v == cluster_name][0] if cluster_name else None except IndexError, e: vsphere_client.disconnect() module.fail_json(msg="Cannot find Cluster named: %s" % From 187a4bd5054cf23cd14a94697836e4d1dcd95551 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 28 Nov 2015 12:38:37 -0500 Subject: [PATCH 21/43] Add support for network, boot_from_volume and volumes nics is a great flexible parameter, but it's wordy. Shade now supports a simple parameter too, which is just "network" and takes a name or id. Add passthrough support. In addition to supporting booting from a pre-existing volume, nova and shade both support the concept of booting from volume based on an image. Pass the parameters through. Shade supports boot-time attachment of additional volumes for OpenStack instances. Pass through the parameter so that ansible users can also take advantage of this. --- cloud/openstack/os_server.py | 97 +++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 7 deletions(-) diff --git a/cloud/openstack/os_server.py b/cloud/openstack/os_server.py index 189840e2498..0d0c5566f96 100644 --- a/cloud/openstack/os_server.py +++ b/cloud/openstack/os_server.py @@ -80,6 +80,13 @@ options: added. This may be a YAML list or a common separated string. required: false default: None + network: + description: + - Name or ID of a network to attach this instance to. A simpler + version of the nics parameter, only one of network or nics should + be supplied. + required: false + default: None nics: description: - A list of networks to which the instance's interface should @@ -87,6 +94,7 @@ options: or port-name. - 'Also this accepts a string containing a list of (net/port)-(id/name) Eg: nics: "net-id=uuid-1,port-name=myport"' + Only one of network or nics should be supplied. required: false default: None auto_ip: @@ -133,15 +141,32 @@ options: - Opaque blob of data which is made available to the instance required: false default: None - root_volume: + boot_from_volume: + description: + - Should the instance boot from a persistent volume created based on + the image given. Mututally exclusive with boot_volume. + required: false + default: false + volume_size: description: - - Boot instance from a volume + - The size of the volume to create in GB if booting from volume based + on an image. + boot_volume: + description: + - Volume name or id to use as the volume to boot from. Implies + boot_from_volume. Mutually exclusive with image and boot_from_volume. required: false default: None + aliases: ['root_volume'] terminate_volume: description: - If true, delete volume when deleting instance (if booted from volume) default: false + volumes: + description: + - A list of preexisting volumes names or ids to attach to the instance + required: false + default: [] state: description: - Should the resource be present or absent. @@ -280,6 +305,52 @@ EXAMPLES = ''' - net-id: 34605f38-e52a-25d2-b6ec-754a13ffb723 - net-name: another_network meta: "hostname=test1,group=uge_master" + +# Creates a new instance and attaches to a specific network +- os_server: + state: present + auth: + auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/ + username: admin + password: admin + project_name: admin + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + key_name: ansible_key + timeout: 200 + flavor: 4 + network: another_network + +# Creates a new instance with 4G of RAM on a 75G Ubuntu Trusty volume +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + os_server: + name: vm1 + state: present + cloud: mordred + region_name: ams01 + image: Ubuntu Server 14.04 + flavor_ram: 4096 + boot_from_volume: True + volume_size: 75 + +# Creates a new instance with 2 volumes attached +- name: launch a compute instance + hosts: localhost + tasks: + - name: launch an instance + os_server: + name: vm1 + state: present + cloud: mordred + region_name: ams01 + image: Ubuntu Server 14.04 + flavor_ram: 4096 + volumes: + - photos + - music ''' @@ -339,7 +410,7 @@ def _create_server(module, cloud): flavor_include = module.params['flavor_include'] image_id = None - if not module.params['root_volume']: + if not module.params['boot_volume']: image_id = cloud.get_image_id( module.params['image'], module.params['image_exclude']) @@ -371,7 +442,9 @@ def _create_server(module, cloud): userdata=module.params['userdata'], config_drive=module.params['config_drive'], ) - for optional_param in ('region_name', 'key_name', 'availability_zone'): + for optional_param in ( + 'region_name', 'key_name', 'availability_zone', 'network', + 'volume_size', 'volumes'): if module.params[optional_param]: bootkwargs[optional_param] = module.params[optional_param] @@ -379,7 +452,8 @@ def _create_server(module, cloud): ip_pool=module.params['floating_ip_pools'], ips=module.params['floating_ips'], auto_ip=module.params['auto_ip'], - root_volume=module.params['root_volume'], + boot_volume=module.params['boot_volume'], + boot_from_volume=module.params['boot_from_volume'], terminate_volume=module.params['terminate_volume'], wait=module.params['wait'], timeout=module.params['timeout'], **bootkwargs @@ -461,6 +535,7 @@ def main(): flavor_include = dict(default=None), key_name = dict(default=None), security_groups = dict(default=['default'], type='list'), + network = dict(default=None), nics = dict(default=[], type='list'), meta = dict(default=None), userdata = dict(default=None), @@ -468,8 +543,11 @@ def main(): auto_ip = dict(default=True, type='bool', aliases=['auto_floating_ip', 'public_ip']), floating_ips = dict(default=None), floating_ip_pools = dict(default=None), - root_volume = dict(default=None), + volume_size = dict(default=False, type='int'), + boot_from_volume = dict(default=False, type='bool'), + boot_volume = dict(default=None, aliases=['root_volume']), terminate_volume = dict(default=False, type='bool'), + volumes = dict(default=[], type='list'), state = dict(default='present', choices=['absent', 'present']), ) module_kwargs = openstack_module_kwargs( @@ -478,7 +556,12 @@ def main(): ['auto_ip', 'floating_ip_pools'], ['floating_ips', 'floating_ip_pools'], ['flavor', 'flavor_ram'], - ['image', 'root_volume'], + ['image', 'boot_volume'], + ['boot_from_volume', 'boot_volume'], + ['nics', 'network'], + ], + required_if=[ + ('boot_from_volume', True, ['volume_size', 'image']), ], ) module = AnsibleModule(argument_spec, **module_kwargs) From 6b13da738bb7d629eef8cd624dce9b9b41eefca5 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 15 Dec 2015 08:43:17 -0500 Subject: [PATCH 22/43] updated module docs, added choices to state --- cloud/amazon/ec2_vpc.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/cloud/amazon/ec2_vpc.py b/cloud/amazon/ec2_vpc.py index a3003a6dcc6..f67909ad233 100644 --- a/cloud/amazon/ec2_vpc.py +++ b/cloud/amazon/ec2_vpc.py @@ -49,19 +49,15 @@ options: - 'A dictionary array of subnets to add of the form: { cidr: ..., az: ... , resource_tags: ... }. Where az is the desired availability zone of the subnet, but it is not required. Tags (i.e.: resource_tags) is also optional and use dictionary form: { "Environment":"Dev", "Tier":"Web", ...}. All VPC subnets not in this list will be removed. As of 1.8, if the subnets parameter is not specified, no existing subnets will be modified.' required: false default: null - aliases: [] vpc_id: description: - A VPC id to terminate when state=absent required: false default: null - aliases: [] resource_tags: description: - 'A dictionary array of resource tags of the form: { tag1: value1, tag2: value2 }. Tags in this list are used in conjunction with CIDR block to uniquely identify a VPC in lieu of vpc_id. Therefore, if CIDR/Tag combination does not exist, a new VPC will be created. VPC tags not on this list will be ignored. Prior to 1.7, specifying a resource tag was optional.' required: true - default: null - aliases: [] version_added: "1.6" internet_gateway: description: @@ -69,31 +65,26 @@ options: required: false default: "no" choices: [ "yes", "no" ] - aliases: [] route_tables: description: - 'A dictionary array of route tables to add of the form: { subnets: [172.22.2.0/24, 172.22.3.0/24,], routes: [{ dest: 0.0.0.0/0, gw: igw},], resource_tags: ... }. Where the subnets list is those subnets the route table should be associated with, and the routes list is a list of routes to be in the table. The special keyword for the gw of igw specifies that you should the route should go through the internet gateway attached to the VPC. gw also accepts instance-ids in addition igw. resource_tags is optional and uses dictionary form: { "Name": "public", ... }. This module is currently unable to affect the "main" route table due to some limitations in boto, so you must explicitly define the associated subnets or they will be attached to the main table implicitly. As of 1.8, if the route_tables parameter is not specified, no existing routes will be modified.' required: false default: null - aliases: [] wait: description: - wait for the VPC to be in state 'available' before returning required: false default: "no" choices: [ "yes", "no" ] - aliases: [] wait_timeout: description: - how long before wait gives up, in seconds default: 300 - aliases: [] state: description: - Create or terminate the VPC required: true - default: present - aliases: [] + choices: [ "present", "absent" ] author: "Carson Gee (@carsongee)" extends_documentation_fragment: - aws From cf061dd93a6e42bdb0d47bbd4145a0dc79427b5a Mon Sep 17 00:00:00 2001 From: Donovan Jones Date: Wed, 16 Dec 2015 15:20:55 +1300 Subject: [PATCH 23/43] fix typo in os_server security_groups description --- cloud/openstack/os_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/openstack/os_server.py b/cloud/openstack/os_server.py index 0d0c5566f96..546e2b1644f 100644 --- a/cloud/openstack/os_server.py +++ b/cloud/openstack/os_server.py @@ -77,7 +77,7 @@ options: security_groups: description: - Names of the security groups to which the instance should be - added. This may be a YAML list or a common separated string. + added. This may be a YAML list or a comma separated string. required: false default: None network: From 85a19c68bd8d5dd6c85342b66ef9b370c67bfbbf Mon Sep 17 00:00:00 2001 From: Lee H Date: Wed, 16 Dec 2015 02:03:30 -0500 Subject: [PATCH 24/43] - remove user_anonymous as the same thing can be accomplished by user='', but leave in the examples for removing anonymous users --- database/mysql/mysql_user.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index aa7f19a4415..09edf8100e7 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -30,13 +30,6 @@ options: description: - name of the user (role) to add or remove required: true - user_anonymous: - description: - - username is to be ignored and anonymous users with no username - handled - required: false - choices: [ "yes", "no" ] - default: no password: description: - set the user's password. (Required when adding a user) @@ -147,11 +140,11 @@ author: "Jonathan Mainguy (@Jmainguy)" ''' EXAMPLES = """ -# Removes anonymous user account for localhost (the name parameter is required, but ignored) -- mysql_user: name=anonymous user_anonymous=yes host=localhost state=absent +# Removes anonymous user account for localhost +- mysql_user: name='' host=localhost state=absent # Removes all anonymous user accounts -- mysql_user: name=anonymous user_anonymous=yes host_all=yes state=absent +- mysql_user: name='' host_all=yes state=absent # Create database user with name 'bob' and password '12345' with all database privileges - mysql_user: name=bob password=12345 priv=*.*:ALL state=present @@ -526,7 +519,6 @@ def main(): login_port=dict(default=3306, type='int'), login_unix_socket=dict(default=None), user=dict(required=True, aliases=['name']), - user_anonymous=dict(type="bool", default="no"), password=dict(default=None, no_log=True), encrypted=dict(default=False, type='bool'), host=dict(default="localhost"), @@ -542,7 +534,6 @@ def main(): login_user = module.params["login_user"] login_password = module.params["login_password"] user = module.params["user"] - user_anonymous = module.params["user_anonymous"] password = module.params["password"] encrypted = module.boolean(module.params["encrypted"]) host = module.params["host"].lower() @@ -554,9 +545,6 @@ def main(): append_privs = module.boolean(module.params["append_privs"]) update_password = module.params['update_password'] - if user_anonymous: - user = '' - config_file = os.path.expanduser(os.path.expandvars(config_file)) if not mysqldb_found: module.fail_json(msg="the python mysqldb module is required") From f3b2180e422eab2de74fa789be65f116070e88e8 Mon Sep 17 00:00:00 2001 From: Lee H Date: Wed, 16 Dec 2015 02:06:02 -0500 Subject: [PATCH 25/43] - add version_added as requested to host_all --- database/mysql/mysql_user.py | 1 + 1 file changed, 1 insertion(+) diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index 09edf8100e7..528f7fadd60 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -55,6 +55,7 @@ options: required: false choices: [ "yes", "no" ] default: "no" + version_added: "2.1" login_user: description: - The username used to authenticate with From f04cd88d22f86524d6a94d9791ae499af126e67f Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 16 Dec 2015 08:06:29 -0800 Subject: [PATCH 26/43] Fix os_server docs build --- cloud/openstack/os_server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/openstack/os_server.py b/cloud/openstack/os_server.py index 546e2b1644f..076af3d8dd4 100644 --- a/cloud/openstack/os_server.py +++ b/cloud/openstack/os_server.py @@ -93,8 +93,8 @@ options: be attached. Networks may be referenced by net-id/net-name/port-id or port-name. - 'Also this accepts a string containing a list of (net/port)-(id/name) - Eg: nics: "net-id=uuid-1,port-name=myport"' - Only one of network or nics should be supplied. + Eg: nics: "net-id=uuid-1,port-name=myport" + Only one of network or nics should be supplied.' required: false default: None auto_ip: From 69d56c4d218594613b2f95a8714d6f13d040e083 Mon Sep 17 00:00:00 2001 From: Jonathan Mainguy Date: Tue, 10 Nov 2015 23:13:48 -0500 Subject: [PATCH 27/43] Unify all 3 mysql modules. Use same connection method, use config_file, and add ssl support --- database/mysql/mysql_db.py | 180 +++++++----------------------- database/mysql/mysql_user.py | 78 ++----------- database/mysql/mysql_variables.py | 125 ++++----------------- 3 files changed, 75 insertions(+), 308 deletions(-) diff --git a/database/mysql/mysql_db.py b/database/mysql/mysql_db.py index b85526e9524..b7317e91082 100644 --- a/database/mysql/mysql_db.py +++ b/database/mysql/mysql_db.py @@ -35,31 +35,6 @@ options: required: true default: null aliases: [ db ] - 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. Requires login_host be defined as other then localhost if login_port is used - required: false - default: 3306 - login_unix_socket: - description: - - The path to a Unix domain socket for local connections - required: false - default: null state: description: - The database state @@ -81,19 +56,8 @@ options: - Location, on the remote host, of the dump file to read from or write to. Uncompressed SQL files (C(.sql)) as well as bzip2 (C(.bz2)), gzip (C(.gz)) and xz (Added in 2.0) compressed files are supported. required: false -notes: - - Requires the MySQLdb Python package on the remote host. For Ubuntu, this - is as easy as apt-get install python-mysqldb. (See M(apt).) For CentOS/Fedora, this - is as easy as yum install MySQL-python. (See M(yum).) - - Requires the mysql command line client. For Centos/Fedora, this is as easy as - yum install mariadb (See M(yum).). For Debian/Ubuntu this is as easy as - apt-get install mariadb-client. (See M(apt).) - - Both I(login_password) and I(login_user) 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 C(root) with no password. -requirements: [ ConfigParser ] author: "Ansible Core Team" +extends_documentation_fragment: mysql ''' EXAMPLES = ''' @@ -111,11 +75,11 @@ EXAMPLES = ''' - mysql_db: state=import name=all target=/tmp/{{ inventory_hostname }}.sql ''' -import ConfigParser import os import pipes import stat import subprocess + try: import MySQLdb except ImportError: @@ -136,9 +100,20 @@ def db_delete(cursor, db): cursor.execute(query) return True -def db_dump(module, host, user, password, db_name, target, all_databases, port, socket=None): +def db_dump(module, host, user, password, db_name, target, all_databases, port, config_file, socket=None, ssl_cert=None, ssl_key=None, ssl_ca=None): cmd = module.get_bin_path('mysqldump', True) - cmd += " --quick --user=%s --password=%s" % (pipes.quote(user), pipes.quote(password)) + # If defined, mysqldump demands --defaults-extra-file be the first option + cmd += " --defaults-extra-file=%s --quick" % pipes.quote(config_file) + if user is not None: + cmd += " --user=%s" % pipes.quote(user) + if password is not None: + cmd += " --password=%s" % pipes.quote(password) + if ssl_cert is not None: + cmd += " --ssl-cert=%s" % pipes.quote(ssl_cert) + if ssl_key is not None: + cmd += " --ssl-key=%s" % pipes.quote(ssl_key) + if ssl_cert is not None: + cmd += " --ssl-ca=%s" % pipes.quote(ssl_ca) if socket is not None: cmd += " --socket=%s" % pipes.quote(socket) else: @@ -164,17 +139,25 @@ def db_dump(module, host, user, password, db_name, target, all_databases, port, rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True) return rc, stdout, stderr -def db_import(module, host, user, password, db_name, target, all_databases, port, socket=None): +def db_import(module, host, user, password, db_name, target, all_databases, port, config_file, socket=None, ssl_cert=None, ssl_key=None, ssl_ca=None): if not os.path.exists(target): return module.fail_json(msg="target %s does not exist on the host" % target) cmd = [module.get_bin_path('mysql', True)] + # --defaults-file must go first, or errors out + cmd.append("--defaults-extra-file=%s" % pipes.quote(config_file)) if user: cmd.append("--user=%s" % pipes.quote(user)) if password: cmd.append("--password=%s" % pipes.quote(password)) if socket is not None: cmd.append("--socket=%s" % pipes.quote(socket)) + if ssl_cert is not None: + cmd.append("--ssl-cert=%s" % pipes.quote(ssl_cert)) + if ssl_key is not None: + cmd.append("--ssl-key=%s" % pipes.quote(ssl_key)) + if ssl_cert is not None: + cmd.append("--ssl-ca=%s" % pipes.quote(ssl_ca)) else: cmd.append("--host=%s" % pipes.quote(host)) cmd.append("--port=%i" % port) @@ -218,61 +201,6 @@ def db_create(cursor, db, encoding, collation): res = cursor.execute(query, query_params) return True -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 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 - # 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 - try: - creds = dict(user=config_get(config, 'client', 'user'),passwd=passwd) - except (ConfigParser.NoOptionError): - return False - return creds - # =========================================== # Module execution. # @@ -290,6 +218,10 @@ def main(): collation=dict(default=""), target=dict(default=None), state=dict(default="present", choices=["absent", "present","dump", "import"]), + ssl_cert=dict(default=None), + ssl_key=dict(default=None), + ssl_ca=dict(default=None), + config_file=dict(default="~/.my.cnf"), ) ) @@ -305,62 +237,37 @@ def main(): login_port = module.params["login_port"] if login_port < 0 or login_port > 65535: module.fail_json(msg="login_port must be a valid unix port number (0-65535)") + ssl_cert = module.params["ssl_cert"] + ssl_key = module.params["ssl_key"] + ssl_ca = module.params["ssl_ca"] + config_file = module.params['config_file'] + config_file = os.path.expanduser(os.path.expandvars(config_file)) + login_password = module.params["login_password"] + login_user = module.params["login_user"] + login_host = module.params["login_host"] # make sure the target path is expanded for ~ and $HOME if target is not None: target = os.path.expandvars(os.path.expanduser(target)) - # 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") - login_host = module.params["login_host"] - if state in ['dump','import']: if target is None: module.fail_json(msg="with state=%s target is required" % (state)) if db == 'all': - connect_to_db = 'mysql' db = 'mysql' all_databases = True else: - connect_to_db = db all_databases = False else: if db == 'all': module.fail_json(msg="name is not allowed to equal 'all' unless state equals import, or dump.") - connect_to_db = '' try: - if socket: - try: - socketmode = os.stat(socket).st_mode - if not stat.S_ISSOCK(socketmode): - module.fail_json(msg="%s, is not a socket, unable to connect" % socket) - except OSError: - module.fail_json(msg="%s, does not exist, unable to connect" % socket) - db_connection = MySQLdb.connect(host=module.params["login_host"], unix_socket=socket, user=login_user, passwd=login_password, db=connect_to_db) - elif login_port != 3306 and module.params["login_host"] == "localhost": - module.fail_json(msg="login_host is required when login_port is defined, login_host cannot be localhost when login_port is defined") - else: - db_connection = MySQLdb.connect(host=module.params["login_host"], port=login_port, user=login_user, passwd=login_password, db=connect_to_db) - cursor = db_connection.cursor() + cursor = mysql_connect(module, login_user, login_password, config_file, ssl_cert, ssl_key, ssl_ca) except Exception, e: - errno, errstr = e.args - if "Unknown database" in str(e): - module.fail_json(msg="ERROR: %s %s" % (errno, errstr)) + if os.path.exists(config_file): + module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or %s has the credentials. Exception message: %s" % (config_file, e)) else: - module.fail_json(msg="unable to connect, check login credentials (login_user, and login_password, which can be defined in ~/.my.cnf), check that mysql socket exists and mysql server is running (ERROR: %s %s)" % (errno, errstr)) + module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, e)) changed = False if db_exists(cursor, db): @@ -372,8 +279,7 @@ def main(): elif state == "dump": rc, stdout, stderr = db_dump(module, login_host, login_user, login_password, db, target, all_databases, - port=login_port, - socket=module.params['login_unix_socket']) + login_port, config_file, socket, ssl_cert, ssl_key, ssl_ca) if rc != 0: module.fail_json(msg="%s" % stderr) else: @@ -381,8 +287,7 @@ def main(): elif state == "import": rc, stdout, stderr = db_import(module, login_host, login_user, login_password, db, target, all_databases, - port=login_port, - socket=module.params['login_unix_socket']) + login_port, config_file, socket, ssl_cert, ssl_key, ssl_ca) if rc != 0: module.fail_json(msg="%s" % stderr) else: @@ -399,5 +304,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.database import * +from ansible.module_utils.mysql import * if __name__ == '__main__': main() diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index 528f7fadd60..fdf6b577d54 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -56,32 +56,6 @@ options: choices: [ "yes", "no" ] default: "no" version_added: "2.1" - 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)" @@ -116,19 +90,7 @@ options: version_added: "2.0" description: - C(always) will update passwords if they differ. C(on_create) will only set the password for newly created users. - config_file: - description: - - Specify a config file from which user and password are to be read - required: false - default: '~/.my.cnf' - version_added: "2.0" 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_user) 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 @@ -136,8 +98,8 @@ notes: the file." - Currently, there is only support for the `mysql_native_password` encryted password hash module. -requirements: [ "MySQLdb" ] author: "Jonathan Mainguy (@Jmainguy)" +extends_documentation_fragment: mysql ''' EXAMPLES = """ @@ -212,30 +174,6 @@ class InvalidPrivsError(Exception): # MySQL module specific support methods. # -def connect(module, login_user=None, login_password=None, config_file=''): - config = { - 'host': module.params['login_host'], - 'db': 'mysql' - } - - if module.params['login_unix_socket']: - config['unix_socket'] = module.params['login_unix_socket'] - else: - config['port'] = module.params['login_port'] - - if os.path.exists(config_file): - config['read_default_file'] = config_file - - # If login_user or login_password are given, they should override the - # config file - if login_user is not None: - config['user'] = login_user - if login_password is not None: - config['passwd'] = login_password - - db_connection = MySQLdb.connect(**config) - return db_connection.cursor() - # User Authentication Management was change in MySQL 5.7 # This is a generic check for if the server version is less than version 5.7 def server_version_check(cursor): @@ -530,6 +468,9 @@ def main(): check_implicit_admin=dict(default=False, type='bool'), update_password=dict(default="always", choices=["always", "on_create"]), config_file=dict(default="~/.my.cnf"), + ssl_cert=dict(default=None), + ssl_key=dict(default=None), + ssl_ca=dict(default=None), ) ) login_user = module.params["login_user"] @@ -545,6 +486,10 @@ def main(): config_file = module.params['config_file'] append_privs = module.boolean(module.params["append_privs"]) update_password = module.params['update_password'] + ssl_cert = module.params["ssl_cert"] + ssl_key = module.params["ssl_key"] + ssl_ca = module.params["ssl_ca"] + db = 'mysql' config_file = os.path.expanduser(os.path.expandvars(config_file)) if not mysqldb_found: @@ -560,14 +505,14 @@ def main(): try: if check_implicit_admin: try: - cursor = connect(module, 'root', '', config_file) + cursor = mysql_connect(module, 'root', '', config_file, ssl_cert, ssl_key, ssl_ca, db) except: pass if not cursor: - cursor = connect(module, login_user, login_password, config_file) + cursor = mysql_connect(module, login_user, login_password, config_file, ssl_cert, ssl_key, ssl_ca, db) 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. Exception message: %s" % e) + module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or %s has the credentials. Exception message: %s" % (config_file, e)) if state == "present": if user_exists(cursor, user, host, host_all): @@ -598,5 +543,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.database import * +from ansible.module_utils.mysql import * if __name__ == '__main__': main() diff --git a/database/mysql/mysql_variables.py b/database/mysql/mysql_variables.py index ab4848d6938..5e551cd0eb3 100644 --- a/database/mysql/mysql_variables.py +++ b/database/mysql/mysql_variables.py @@ -40,26 +40,7 @@ options: description: - If set, then sets variable value to this required: False - login_user: - description: - - username to connect mysql host, if defined login_password also needed. - required: False - login_password: - description: - - password to connect mysql host, if defined login_user also needed. - required: False - login_host: - description: - - mysql host to connect - required: False - login_port: - version_added: "2.0" - description: - - mysql port to connect - required: False - login_unix_socket: - description: - - unix socket to connect mysql server +extends_documentation_fragment: mysql ''' EXAMPLES = ''' # Check for sync_binlog setting @@ -70,7 +51,6 @@ EXAMPLES = ''' ''' -import ConfigParser import os import warnings from re import match @@ -134,66 +114,6 @@ def setvariable(cursor, mysqlvar, value): result = str(e) return result - -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 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 - # 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 main(): module = AnsibleModule( argument_spec = dict( @@ -203,14 +123,24 @@ def main(): login_port=dict(default="3306", type='int'), login_unix_socket=dict(default=None), variable=dict(default=None), - value=dict(default=None) - + value=dict(default=None), + ssl_cert=dict(default=None), + ssl_key=dict(default=None), + ssl_ca=dict(default=None), + config_file=dict(default="~/.my.cnf") ) ) user = module.params["login_user"] password = module.params["login_password"] host = module.params["login_host"] port = module.params["login_port"] + ssl_cert = module.params["ssl_cert"] + ssl_key = module.params["ssl_key"] + ssl_ca = module.params["ssl_ca"] + config_file = module.params['config_file'] + config_file = os.path.expanduser(os.path.expandvars(config_file)) + db = 'mysql' + mysqlvar = module.params["variable"] value = module.params["value"] if mysqlvar is None: @@ -222,30 +152,14 @@ def main(): else: warnings.filterwarnings('error', category=MySQLdb.Warning) - # 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") try: - if module.params["login_unix_socket"]: - db_connection = MySQLdb.connect(host=module.params["login_host"], port=module.params["login_port"], 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=module.params["login_port"], user=login_user, passwd=login_password, db="mysql") - cursor = db_connection.cursor() + cursor = mysql_connect(module, user, password, config_file, ssl_cert, ssl_key, ssl_ca, db) except Exception, e: - errno, errstr = e.args - module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or ~/.my.cnf has the credentials (ERROR: %s %s)" % (errno, errstr)) + if os.path.exists(config_file): + module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or %s has the credentials. Exception message: %s" % (config_file, e)) + else: + module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, e)) + mysqlvar_val = getvariable(cursor, mysqlvar) if mysqlvar_val is None: module.fail_json(msg="Variable not available \"%s\"" % mysqlvar, changed=False) @@ -269,4 +183,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.database import * +from ansible.module_utils.mysql import * main() From 16a3bdaa7da9e9f7c0572d3a3fdbfd79f29c2b9d Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 16 Dec 2015 14:07:17 -0800 Subject: [PATCH 28/43] Account for mariadb versioning --- database/mysql/mysql_user.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index fdf6b577d54..95d11a164df 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -182,10 +182,14 @@ def server_version_check(cursor): version_str = result[0] version = version_str.split('.') + # Currently we have no facility to handle new-style password update on + # mariadb and the old-style update continues to work + if version_str.lower().endswith('mariadb'): + return True if (int(version[0]) <= 5 and int(version[1]) < 7): - return True + return True else: - return False + return False def user_exists(cursor, user, host, host_all): if host_all: From 9cc67e45a68a7f7aa51be009a9391f01922439c9 Mon Sep 17 00:00:00 2001 From: Alberto Gireud Date: Thu, 17 Dec 2015 07:17:44 -0600 Subject: [PATCH 29/43] Update root_volume variable --- cloud/openstack/os_server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/openstack/os_server.py b/cloud/openstack/os_server.py index 076af3d8dd4..f54b150388d 100644 --- a/cloud/openstack/os_server.py +++ b/cloud/openstack/os_server.py @@ -571,14 +571,14 @@ def main(): state = module.params['state'] image = module.params['image'] - root_volume = module.params['root_volume'] + boot_volume = module.params['boot_volume'] flavor = module.params['flavor'] flavor_ram = module.params['flavor_ram'] if state == 'present': - if not (image or root_volume): + if not (image or boot_volume): module.fail_json( - msg="Parameter 'image' or 'root_volume' is required " + msg="Parameter 'image' or 'boot_volume' is required " "if state == 'present'" ) if not flavor and not flavor_ram: From 827b9596da2dce75a245e842db2ac2444744a2ee Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 17 Dec 2015 12:55:43 -0500 Subject: [PATCH 30/43] service goes back to failing when absent if no tools and no init script, this should always fail --- system/service.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/system/service.py b/system/service.py index 2b8dbb8696c..0364766448e 100644 --- a/system/service.py +++ b/system/service.py @@ -471,8 +471,7 @@ class LinuxService(Service): self.enable_cmd = location['chkconfig'] if self.enable_cmd is None: - # exiting without change on non-existent service - self.module.exit_json(changed=False, exists=False) + self.module.fail_json(msg="no service or tool found for: %s" % self.name) # If no service control tool selected yet, try to see if 'service' is available if self.svc_cmd is None and location.get('service', False): @@ -480,7 +479,7 @@ class LinuxService(Service): # couldn't find anything yet if self.svc_cmd is None and not self.svc_initscript: - self.module.exit_json(changed=False, exists=False) + self.module.fail_json(msg='cannot find \'service\' binary or init script for service, possible typo in service name?, aborting') if location.get('initctl', False): self.svc_initctl = location['initctl'] From b4a3fdd493378853c0b6ab35d5d8bcf52612a4a0 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Thu, 17 Dec 2015 11:35:44 -0800 Subject: [PATCH 31/43] Fix mysqldump usage of config_file --- database/mysql/mysql_db.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/database/mysql/mysql_db.py b/database/mysql/mysql_db.py index b7317e91082..24bcf40ed84 100644 --- a/database/mysql/mysql_db.py +++ b/database/mysql/mysql_db.py @@ -103,7 +103,9 @@ def db_delete(cursor, db): def db_dump(module, host, user, password, db_name, target, all_databases, port, config_file, socket=None, ssl_cert=None, ssl_key=None, ssl_ca=None): cmd = module.get_bin_path('mysqldump', True) # If defined, mysqldump demands --defaults-extra-file be the first option - cmd += " --defaults-extra-file=%s --quick" % pipes.quote(config_file) + if config_file: + cmd += " --defaults-extra-file=%s" % pipes.quote(config_file) + cmd += " --quick" if user is not None: cmd += " --user=%s" % pipes.quote(user) if password is not None: @@ -145,7 +147,8 @@ def db_import(module, host, user, password, db_name, target, all_databases, port cmd = [module.get_bin_path('mysql', True)] # --defaults-file must go first, or errors out - cmd.append("--defaults-extra-file=%s" % pipes.quote(config_file)) + if config_file: + cmd.append("--defaults-extra-file=%s" % pipes.quote(config_file)) if user: cmd.append("--user=%s" % pipes.quote(user)) if password: @@ -270,6 +273,8 @@ def main(): module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, e)) changed = False + if not os.path.exists(config_file): + config_file = None if db_exists(cursor, db): if state == "absent": try: From 9366dfb63e565c9e0901d714be8832fc89b275d6 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Thu, 17 Dec 2015 13:45:04 -0800 Subject: [PATCH 32/43] mariadb isn't always the last elemen of the version string --- database/mysql/mysql_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index 95d11a164df..51f6e9ea1d4 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -184,7 +184,7 @@ def server_version_check(cursor): # Currently we have no facility to handle new-style password update on # mariadb and the old-style update continues to work - if version_str.lower().endswith('mariadb'): + if 'mariadb' in version_str.lower(): return True if (int(version[0]) <= 5 and int(version[1]) < 7): return True From 1bd04f797e2da8da8261c067eda09777ddf23fd7 Mon Sep 17 00:00:00 2001 From: Pedro Romano Date: Wed, 1 Oct 2014 10:41:17 +0100 Subject: [PATCH 33/43] GCE libcloud 0.15 support and code cleanup * Code formatting (indentation and white space) fixes for improved PEP8 conformity. * Remove redundant backslashes inside parentheses. * Test for object identity should be 'is not None'. * Test for membership should be 'not in'. * Fit docstring to the PEP8 79 character limit. * Use forward compatible Python 2.6+ 'except .. as' syntax for exception handling. * Support libcloud > 0.15 'metadata' argument format. --- cloud/google/gce.py | 137 ++++++++++++++++++++++++++------------------ 1 file changed, 80 insertions(+), 57 deletions(-) diff --git a/cloud/google/gce.py b/cloud/google/gce.py index 1de351a12fb..d3c60fcec34 100644 --- a/cloud/google/gce.py +++ b/cloud/google/gce.py @@ -44,7 +44,8 @@ options: default: "n1-standard-1" metadata: description: - - a hash/dictionary of custom data for the instance; '{"key":"value",...}' + - a hash/dictionary of custom data for the instance; + '{"key": "value", ...}' required: false default: null service_account_email: @@ -56,10 +57,17 @@ options: service_account_permissions: version_added: "2.0" description: - - service account permissions (see U(https://cloud.google.com/sdk/gcloud/reference/compute/instances/create), --scopes section for detailed information) + - service account permissions (see + U(https://cloud.google.com/sdk/gcloud/reference/compute/instances/create), + --scopes section for detailed information) required: false default: null - choices: ["bigquery", "cloud-platform", "compute-ro", "compute-rw", "computeaccounts-ro", "computeaccounts-rw", "datastore", "logging-write", "monitoring", "sql", "sql-admin", "storage-full", "storage-ro", "storage-rw", "taskqueue", "userinfo-email"] + choices: [ + "bigquery", "cloud-platform", "compute-ro", "compute-rw", + "computeaccounts-ro", "computeaccounts-rw", "datastore", "logging-write", + "monitoring", "sql", "sql-admin", "storage-full", "storage-ro", + "storage-rw", "taskqueue", "userinfo-email" + ] pem_file: version_added: "1.5.1" description: @@ -88,7 +96,10 @@ options: default: "false" disks: description: - - a list of persistent disks to attach to the instance; a string value gives the name of the disk; alternatively, a dictionary value can define 'name' and 'mode' ('READ_ONLY' or 'READ_WRITE'). The first entry will be the boot disk (which must be READ_WRITE). + - a list of persistent disks to attach to the instance; a string value + gives the name of the disk; alternatively, a dictionary value can + define 'name' and 'mode' ('READ_ONLY' or 'READ_WRITE'). The first entry + will be the boot disk (which must be READ_WRITE). required: false default: null version_added: "1.7" @@ -111,7 +122,8 @@ options: ip_forward: version_added: "1.9" description: - - set to true if the instance can forward ip packets (useful for gateways) + - set to true if the instance can forward ip packets (useful for + gateways) required: false default: "false" external_ip: @@ -167,7 +179,8 @@ EXAMPLES = ''' tasks: - name: Launch instances local_action: gce instance_names={{names}} machine_type={{machine_type}} - image={{image}} zone={{zone}} service_account_email={{ service_account_email }} + image={{image}} zone={{zone}} + service_account_email={{ service_account_email }} pem_file={{ pem_file }} project_id={{ project_id }} register: gce - name: Wait for SSH to come up @@ -195,10 +208,11 @@ EXAMPLES = ''' ''' try: + import libcloud from libcloud.compute.types import Provider from libcloud.compute.providers import get_driver from libcloud.common.google import GoogleBaseError, QuotaExceededError, \ - ResourceExistsError, ResourceInUseError, ResourceNotFoundError + ResourceExistsError, ResourceInUseError, ResourceNotFoundError _ = Provider.GCE HAS_LIBCLOUD = True except ImportError: @@ -239,7 +253,7 @@ def get_instance_info(inst): public_ip = inst.public_ips[0] return({ - 'image': not inst.image is None and inst.image.split('/')[-1] or None, + 'image': inst.image is not None and inst.image.split('/')[-1] or None, 'disks': disk_names, 'machine_type': inst.size, 'metadata': metadata, @@ -250,7 +264,8 @@ def get_instance_info(inst): 'status': ('status' in inst.extra) and inst.extra['status'] or None, 'tags': ('tags' in inst.extra) and inst.extra['tags'] or [], 'zone': ('zone' in inst.extra) and inst.extra['zone'].name or None, - }) + }) + def create_instances(module, gce, instance_names): """Creates new instances. Attributes other than instance_names are picked @@ -308,25 +323,31 @@ def create_instances(module, gce, instance_names): # with: # [ {'key': key1, 'value': value1}, {'key': key2, 'value': value2}, ...] if metadata: - try: - md = literal_eval(str(metadata)) - if not isinstance(md, dict): - raise ValueError('metadata must be a dict') - except ValueError, e: - module.fail_json(msg='bad metadata: %s' % str(e)) - except SyntaxError, e: - module.fail_json(msg='bad metadata syntax') - + if isinstance(metadata, dict): + md = metadata + else: + try: + md = literal_eval(str(metadata)) + if not isinstance(md, dict): + raise ValueError('metadata must be a dict') + except ValueError as e: + module.fail_json(msg='bad metadata: %s' % str(e)) + except SyntaxError as e: + module.fail_json(msg='bad metadata syntax') + + if hasattr(libcloud, '__version__') and libcloud.__version__ < '0.15': items = [] - for k,v in md.items(): - items.append({"key": k,"value": v}) + for k, v in md.items(): + items.append({"key": k, "value": v}) metadata = {'items': items} + else: + metadata = md ex_sa_perms = [] bad_perms = [] if service_account_permissions: for perm in service_account_permissions: - if not perm in gce.SA_SCOPES_MAP.keys(): + if perm not in gce.SA_SCOPES_MAP.keys(): bad_perms.append(perm) if len(bad_perms) > 0: module.fail_json(msg='bad permissions: %s' % str(bad_perms)) @@ -339,7 +360,7 @@ def create_instances(module, gce, instance_names): # These variables all have default values but check just in case if not lc_image or not lc_network or not lc_machine_type or not lc_zone: module.fail_json(msg='Missing required create instance variable', - changed=False) + changed=False) for name in instance_names: pd = None @@ -352,16 +373,19 @@ def create_instances(module, gce, instance_names): pd = gce.ex_get_volume("%s" % name, lc_zone) inst = None try: - inst = gce.create_node(name, lc_machine_type, lc_image, - location=lc_zone, ex_network=network, ex_tags=tags, - ex_metadata=metadata, ex_boot_disk=pd, ex_can_ip_forward=ip_forward, - external_ip=external_ip, ex_disk_auto_delete=disk_auto_delete, ex_service_accounts=ex_sa_perms) + inst = gce.create_node( + name, lc_machine_type, lc_image, location=lc_zone, + ex_network=network, ex_tags=tags, ex_metadata=metadata, + ex_boot_disk=pd, ex_can_ip_forward=ip_forward, + external_ip=external_ip, ex_disk_auto_delete=disk_auto_delete, + ex_service_accounts=ex_sa_perms + ) changed = True except ResourceExistsError: inst = gce.ex_get_node(name, lc_zone) - except GoogleBaseError, e: - module.fail_json(msg='Unexpected error attempting to create ' + \ - 'instance %s, error: %s' % (name, e.value)) + except GoogleBaseError as e: + module.fail_json(msg='Unexpected error attempting to create ' + + 'instance %s, error: %s' % (name, e.value)) for i, lc_disk in enumerate(lc_disks): # Check whether the disk is already attached @@ -417,7 +441,7 @@ def terminate_instances(module, gce, instance_names, zone_name): inst = gce.ex_get_node(name, zone_name) except ResourceNotFoundError: pass - except Exception, e: + except Exception as e: module.fail_json(msg=unexpected_error_msg(e), changed=False) if inst: gce.destroy_node(inst) @@ -429,27 +453,27 @@ def terminate_instances(module, gce, instance_names, zone_name): def main(): module = AnsibleModule( - argument_spec = dict( - image = dict(default='debian-7'), - instance_names = dict(), - machine_type = dict(default='n1-standard-1'), - metadata = dict(), - name = dict(), - network = dict(default='default'), - persistent_boot_disk = dict(type='bool', default=False), - disks = dict(type='list'), - state = dict(choices=['active', 'present', 'absent', 'deleted'], - default='present'), - tags = dict(type='list'), - zone = dict(default='us-central1-a'), - service_account_email = dict(), - service_account_permissions = dict(type='list'), - pem_file = dict(), - project_id = dict(), - ip_forward = dict(type='bool', default=False), - external_ip = dict(choices=['ephemeral', 'none'], - default='ephemeral'), - disk_auto_delete = dict(type='bool', default=True), + argument_spec=dict( + image=dict(default='debian-7'), + instance_names=dict(), + machine_type=dict(default='n1-standard-1'), + metadata=dict(), + name=dict(), + network=dict(default='default'), + persistent_boot_disk=dict(type='bool', default=False), + disks=dict(type='list'), + state=dict(choices=['active', 'present', 'absent', 'deleted'], + default='present'), + tags=dict(type='list'), + zone=dict(default='us-central1-a'), + service_account_email=dict(), + service_account_permissions=dict(type='list'), + pem_file=dict(), + project_id=dict(), + ip_forward=dict(type='bool', default=False), + external_ip=dict(choices=['ephemeral', 'none'], + default='ephemeral'), + disk_auto_delete=dict(type='bool', default=True), ) ) @@ -482,15 +506,15 @@ def main(): inames.append(name) if not inames: module.fail_json(msg='Must specify a "name" or "instance_names"', - changed=False) + changed=False) if not zone: module.fail_json(msg='Must specify a "zone"', changed=False) json_output = {'zone': zone} if state in ['absent', 'deleted']: json_output['state'] = 'absent' - (changed, terminated_instance_names) = terminate_instances(module, - gce, inames, zone) + (changed, terminated_instance_names) = terminate_instances( + module, gce, inames, zone) # based on what user specified, return the same variable, although # value could be different if an instance could not be destroyed @@ -501,15 +525,14 @@ def main(): elif state in ['active', 'present']: json_output['state'] = 'present' - (changed, instance_data,instance_name_list) = create_instances( - module, gce, inames) + (changed, instance_data, instance_name_list) = create_instances( + module, gce, inames) json_output['instance_data'] = instance_data if instance_names: json_output['instance_names'] = instance_name_list elif name: json_output['name'] = name - json_output['changed'] = changed module.exit_json(**json_output) From 8fe5d6f7ef7f17f1ec54a44570daf28ebd85f208 Mon Sep 17 00:00:00 2001 From: Joe Quadrino Date: Tue, 3 Nov 2015 09:11:18 -0500 Subject: [PATCH 34/43] add devices parameter for docker module --- cloud/docker/docker.py | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/cloud/docker/docker.py b/cloud/docker/docker.py index 4a3fb238603..a7f924ae289 100644 --- a/cloud/docker/docker.py +++ b/cloud/docker/docker.py @@ -95,6 +95,11 @@ options: - 'alias. Use docker CLI-style syntax: C(redis:myredis).' default: null version_added: "1.5" + devices: + description: + - List of host devices to expose to container + default: null + required: false log_driver: description: - You can specify a different logging driver for the container than for the daemon. @@ -386,6 +391,8 @@ EXAMPLES = ''' # stopped and removed, and a new one will be launched in its place. # - link this container to the existing redis container launched above with # an alias. +# - grant the container read write permissions for the host's /dev/sda device +# through a node named /dev/xvda # - bind TCP port 9000 within the container to port 8080 on all interfaces # on the host. # - bind UDP port 9001 within the container to port 8081 on the host, only @@ -400,6 +407,8 @@ EXAMPLES = ''' pull: always links: - "myredis:aliasedredis" + devices: + - "/dev/sda:/dev/xvda:rwm" ports: - "8080:9000" - "127.0.0.1:8081:9001/udp" @@ -602,6 +611,7 @@ class DockerManager(object): # docker-py version is a tuple of ints because we have to compare them # server APIVersion is passed to a docker-py function that takes strings _cap_ver_req = { + 'devices': ((0, 7, 0), '1.2'), 'dns': ((0, 3, 0), '1.10'), 'volumes_from': ((0, 3, 0), '1.10'), 'restart_policy': ((0, 5, 0), '1.14'), @@ -839,11 +849,15 @@ class DockerManager(object): } optionals = {} - for optional_param in ('dns', 'volumes_from', 'restart_policy', - 'restart_policy_retry', 'pid', 'extra_hosts', 'log_driver', - 'cap_add', 'cap_drop', 'read_only', 'log_opt'): + for optional_param in ('devices', 'dns', 'volumes_from', + 'restart_policy', 'restart_policy_retry', 'pid', 'extra_hosts', + 'log_driver', 'cap_add', 'cap_drop', 'read_only', 'log_opt'): optionals[optional_param] = self.module.params.get(optional_param) + if optionals['devices'] is not None: + self.ensure_capability('devices') + params['devices'] = optionals['devices'] + if optionals['dns'] is not None: self.ensure_capability('dns') params['dns'] = optionals['dns'] @@ -1299,6 +1313,24 @@ class DockerManager(object): differing.append(container) continue + # DEVICES + + expected_devices = set() + for device in (self.module.params.get('devices') or []): + if len(device.split(':')) == 2: + expected_devices.add(device + ":rwm") + else: + expected_devices.add(device) + + actual_devices = set() + for device in (container['HostConfig']['Devices'] or []): + actual_devices.add("{PathOnHost}:{PathInContainer}:{CgroupPermissions}".format(**device)) + + if actual_devices != expected_devices: + self.reload_reasons.append('devices ({0} => {1})'.format(actual_devices, expected_devices)) + differing.append(container) + continue + # DNS expected_dns = set(self.module.params.get('dns') or []) @@ -1667,6 +1699,7 @@ def main(): volumes = dict(default=None, type='list'), volumes_from = dict(default=None), links = dict(default=None, type='list'), + devices = dict(default=None, type='list'), memory_limit = dict(default=0), memory_swap = dict(default=0), docker_url = dict(), From 581b4f6de6eb633ec9df3615629f86df3120fa47 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 18 Dec 2015 16:33:48 -0800 Subject: [PATCH 35/43] Add version_added to documentation --- cloud/docker/docker.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cloud/docker/docker.py b/cloud/docker/docker.py index a7f924ae289..0902aaf92b7 100644 --- a/cloud/docker/docker.py +++ b/cloud/docker/docker.py @@ -100,6 +100,7 @@ options: - List of host devices to expose to container default: null required: false + version_added: "2.1" log_driver: description: - You can specify a different logging driver for the container than for the daemon. @@ -391,7 +392,7 @@ EXAMPLES = ''' # stopped and removed, and a new one will be launched in its place. # - link this container to the existing redis container launched above with # an alias. -# - grant the container read write permissions for the host's /dev/sda device +# - grant the container read write permissions for the host's /dev/sda device # through a node named /dev/xvda # - bind TCP port 9000 within the container to port 8080 on all interfaces # on the host. @@ -849,8 +850,8 @@ class DockerManager(object): } optionals = {} - for optional_param in ('devices', 'dns', 'volumes_from', - 'restart_policy', 'restart_policy_retry', 'pid', 'extra_hosts', + for optional_param in ('devices', 'dns', 'volumes_from', + 'restart_policy', 'restart_policy_retry', 'pid', 'extra_hosts', 'log_driver', 'cap_add', 'cap_drop', 'read_only', 'log_opt'): optionals[optional_param] = self.module.params.get(optional_param) @@ -1313,7 +1314,7 @@ class DockerManager(object): differing.append(container) continue - # DEVICES + # DEVICES expected_devices = set() for device in (self.module.params.get('devices') or []): From 19ebc453647d99245a6a37ffd6040193f47baa67 Mon Sep 17 00:00:00 2001 From: Omar Khan Date: Thu, 24 Sep 2015 12:15:59 +0700 Subject: [PATCH 36/43] Add stop_timeout option to docker module --- cloud/docker/docker.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cloud/docker/docker.py b/cloud/docker/docker.py index 3727d305b04..f5a1f0db24a 100644 --- a/cloud/docker/docker.py +++ b/cloud/docker/docker.py @@ -341,6 +341,12 @@ options: required: false default: null version_added: "1.9.4" + stop_timeout: + description: + - How many seconds to wait for the container to stop before killing it. + required: false + default: 10 + version_added: "2.0" author: - "Cove Schneider (@cove)" - "Joshua Conner (@joshuaconner)" @@ -626,6 +632,7 @@ class DockerManager(object): 'cap_drop': ((0, 5, 0), '1.14'), 'read_only': ((1, 0, 0), '1.17'), 'labels': ((1, 2, 0), '1.18'), + 'stop_timeout': ((0, 5, 0), '1.0'), # Clientside only 'insecure_registry': ((0, 5, 0), '0.0') } @@ -1542,7 +1549,7 @@ class DockerManager(object): def stop_containers(self, containers): for i in containers: - self.client.stop(i['Id']) + self.client.stop(i['Id'], self.module.params.get('stop_timeout')) self.increment_counter('stopped') return [self.client.wait(i['Id']) for i in containers] @@ -1745,6 +1752,7 @@ def main(): cap_drop = dict(default=None, type='list'), read_only = dict(default=None, type='bool'), labels = dict(default={}, type='dict'), + stop_timeout = dict(default=10, type='int'), ), required_together = ( ['tls_client_cert', 'tls_client_key'], From ba3ee25172ef09de5b187dc56653945a843fd668 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 18 Dec 2015 17:57:17 -0800 Subject: [PATCH 37/43] labels is actually not aded in 1.9.4 but in 2.1 --- cloud/docker/docker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/docker/docker.py b/cloud/docker/docker.py index f5a1f0db24a..b95c7ba5706 100644 --- a/cloud/docker/docker.py +++ b/cloud/docker/docker.py @@ -340,7 +340,7 @@ options: - Set container labels. Requires docker >= 1.6 and docker-py >= 1.2.0. required: false default: null - version_added: "1.9.4" + version_added: "2.1" stop_timeout: description: - How many seconds to wait for the container to stop before killing it. From 15c1c0cca79196d4dde630db2a7eee90367051cc Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 18 Dec 2015 21:28:01 -0800 Subject: [PATCH 38/43] entrypoint feature added in 2.1 --- cloud/docker/docker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/docker/docker.py b/cloud/docker/docker.py index fe1ef4707d1..86402bee773 100644 --- a/cloud/docker/docker.py +++ b/cloud/docker/docker.py @@ -53,7 +53,7 @@ options: Used to match and launch containers. default: null required: false - version_added: "2.0" + version_added: "2.1" command: description: - Command used to match and launch containers. From fcb3397df7944ff15ea698b5717c06e8fc7d43ba Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 18 Dec 2015 22:22:55 -0800 Subject: [PATCH 39/43] Fix up documentation --- cloud/google/gce.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/google/gce.py b/cloud/google/gce.py index d3c60fcec34..fcaa3b85023 100644 --- a/cloud/google/gce.py +++ b/cloud/google/gce.py @@ -45,7 +45,7 @@ options: metadata: description: - a hash/dictionary of custom data for the instance; - '{"key": "value", ...}' + '{"key":"value", ...}' required: false default: null service_account_email: From 3281cad95e6cf4fa2f213c641102bf4d7bc16b2f Mon Sep 17 00:00:00 2001 From: "T.Kuramochi" Date: Fri, 28 Aug 2015 14:46:08 +0900 Subject: [PATCH 40/43] Add HTTP Proxy options Update a document file for win_get_url.ps1. Update add a prefix proxy_ for this variables Update a document file for win_get_url.ps1. Update win_get_url.ps1 20150907 --- windows/win_get_url.ps1 | 12 ++++++++++++ windows/win_get_url.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/windows/win_get_url.ps1 b/windows/win_get_url.ps1 index a83ad2633b0..b7b1a1ed445 100644 --- a/windows/win_get_url.ps1 +++ b/windows/win_get_url.ps1 @@ -44,6 +44,10 @@ $skip_certificate_validation = Get-Attr $params "skip_certificate_validation" $f $username = Get-Attr $params "username" $password = Get-Attr $params "password" +$proxy_url = $params.proxy_url +$proxy_username = $params.proxy_username +$proxy_password = $params.proxy_password + if($skip_certificate_validation){ [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} } @@ -52,6 +56,14 @@ $force = Get-Attr -obj $params -name "force" "yes" | ConvertTo-Bool If ($force -or -not (Test-Path $dest)) { $client = New-Object System.Net.WebClient + if($params.proxy_url) { + $proxy_url = $params.proxy_url + if($proxy_username -and $proxy_password){ + $proxy_credential = New-Object System.Net.NetworkCredential($proxy_username, $proxy_password) + $proxy_server.Credentials = $proxy_credential + } + $client.Proxy = $proxy_server + } if($username -and $password){ $client.Credentials = New-Object System.Net.NetworkCredential($username, $password) diff --git a/windows/win_get_url.py b/windows/win_get_url.py index 5c3e994d418..cfe93982f61 100644 --- a/windows/win_get_url.py +++ b/windows/win_get_url.py @@ -65,6 +65,30 @@ options: - Skip SSL certificate validation if true required: false default: false + proxy_url: + description: + - The full URL of the proxy server a file to download through it. + version_added: "2.0" + required: false + choices: null + default: null + proxy_username: + description: + - Name of the user for authorization of the proxy server. + version_added: "2.0" + required: false + choices: null + default: null + proxy_password: + description: + - Password of the user for authorization of the proxy server. + version_added: "2.0" + required: false + choices: null + default: null +author: + - "Paul Durivage (@angstwad)" + - "Takeshi Kuramochi (tksarah)" ''' EXAMPLES = ''' @@ -83,4 +107,12 @@ $ ansible -i hosts -c winrm -m win_get_url -a "url=http://www.example.com/earthr url: 'http://www.example.com/earthrise.jpg' dest: 'C:\Users\RandomUser\earthrise.jpg' force: no + +- name: Download earthrise.jpg to 'C:\Users\RandomUser\earthrise.jpg' through the proxy server. + win_get_url: + url: 'http://www.example.com/earthrise.jpg' + dest: 'C:\Users\RandomUser\earthrise.jpg' + proxy_url: 'http://10.0.0.1:8080' + proxy_username: 'username' + proxy_password: 'password' ''' From c9fe542e8c1a2833fa640beb0c7595dcf4c4f0f8 Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Sat, 19 Dec 2015 00:19:16 -0800 Subject: [PATCH 41/43] minor win_get_url doc update --- windows/win_get_url.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/windows/win_get_url.py b/windows/win_get_url.py index cfe93982f61..1e2031170e1 100644 --- a/windows/win_get_url.py +++ b/windows/win_get_url.py @@ -67,21 +67,21 @@ options: default: false proxy_url: description: - - The full URL of the proxy server a file to download through it. + - The full URL of the proxy server to download through. version_added: "2.0" required: false choices: null default: null proxy_username: description: - - Name of the user for authorization of the proxy server. + - Proxy authentication username version_added: "2.0" required: false choices: null default: null proxy_password: description: - - Password of the user for authorization of the proxy server. + - Proxy authentication password version_added: "2.0" required: false choices: null @@ -108,7 +108,7 @@ $ ansible -i hosts -c winrm -m win_get_url -a "url=http://www.example.com/earthr dest: 'C:\Users\RandomUser\earthrise.jpg' force: no -- name: Download earthrise.jpg to 'C:\Users\RandomUser\earthrise.jpg' through the proxy server. +- name: Download earthrise.jpg to 'C:\Users\RandomUser\earthrise.jpg' through a proxy server. win_get_url: url: 'http://www.example.com/earthrise.jpg' dest: 'C:\Users\RandomUser\earthrise.jpg' From 11f4340a441547e09248e116324fa16d16f12caa Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Sat, 19 Dec 2015 01:11:35 -0800 Subject: [PATCH 42/43] win_get_url doc/strict-mode fixes plus cleaning up from bad merge --- windows/win_get_url.ps1 | 10 +++++----- windows/win_get_url.py | 6 ------ 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/windows/win_get_url.ps1 b/windows/win_get_url.ps1 index b7b1a1ed445..71a4d5751af 100644 --- a/windows/win_get_url.ps1 +++ b/windows/win_get_url.ps1 @@ -44,9 +44,9 @@ $skip_certificate_validation = Get-Attr $params "skip_certificate_validation" $f $username = Get-Attr $params "username" $password = Get-Attr $params "password" -$proxy_url = $params.proxy_url -$proxy_username = $params.proxy_username -$proxy_password = $params.proxy_password +$proxy_url = Get-Attr $params "proxy_url" +$proxy_username = Get-Attr $params "proxy_username" +$proxy_password = Get-Attr $params "proxy_password" if($skip_certificate_validation){ [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} @@ -56,8 +56,8 @@ $force = Get-Attr -obj $params -name "force" "yes" | ConvertTo-Bool If ($force -or -not (Test-Path $dest)) { $client = New-Object System.Net.WebClient - if($params.proxy_url) { - $proxy_url = $params.proxy_url + if($proxy_url) { + $proxy_server = New-Object System.Net.WebProxy($proxy_url, $true) if($proxy_username -and $proxy_password){ $proxy_credential = New-Object System.Net.NetworkCredential($proxy_username, $proxy_password) $proxy_server.Credentials = $proxy_credential diff --git a/windows/win_get_url.py b/windows/win_get_url.py index 1e2031170e1..26b0dc7b012 100644 --- a/windows/win_get_url.py +++ b/windows/win_get_url.py @@ -70,22 +70,16 @@ options: - The full URL of the proxy server to download through. version_added: "2.0" required: false - choices: null - default: null proxy_username: description: - Proxy authentication username version_added: "2.0" required: false - choices: null - default: null proxy_password: description: - Proxy authentication password version_added: "2.0" required: false - choices: null - default: null author: - "Paul Durivage (@angstwad)" - "Takeshi Kuramochi (tksarah)" From 6e9adc46870816eb08f686537612ab1e319da504 Mon Sep 17 00:00:00 2001 From: Sam Thursfield Date: Fri, 29 May 2015 16:59:12 +0000 Subject: [PATCH 43/43] os_server: Add some error checking for the 'nics' parameter If this parameter was not of the right type, the module would fail with a traceback, with a "AttributeError: 'str' object has no attribute 'get'" exception. It now gives a proper error message on type errors. --- cloud/openstack/os_server.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cloud/openstack/os_server.py b/cloud/openstack/os_server.py index f54b150388d..036d4edded7 100644 --- a/cloud/openstack/os_server.py +++ b/cloud/openstack/os_server.py @@ -372,7 +372,14 @@ def _network_args(module, cloud): args = [] nics = module.params['nics'] + if type(nics) != list: + module.fail_json(msg='The \'nics\' parameter must be a list.') + for net in _parse_nics(nics): + if type(net) != dict: + module.fail_json( + msg='Each entry in the \'nics\' parameter must be a dict.') + if net.get('net-id'): args.append(net) elif net.get('net-name'):